On this page |
|
Overview ¶
(See Python states for the basics of how to implement a custom viewer state.)
The standard way to allow user interaction with a node is to bind handles to node parameters in a node’s state. Handles can be very powerful on their own, letting you specify a ready-made user interface for a wide variety of parameter setups.
However, sometimes you want the ability to interpret lower-level user input such as mouse moves, button presses, mouse wheel clicks, tablet tilt and pressure, and so on. You can implement the onMouseEvent
callback to handle low-level events.
-
While your state is active, in your mouse event handler you can get a ray (a directional line) from the mouse “into” the screen. You can intersect this pointing ray with the geometry to know what is under the mouse pointer at that moment.
-
To set up a right-click context menu for your custom state, see Python state menu.
-
The UI device object has useful methods for detecting modifier keys and arrow keys. For information on capturing hotkeys, see Python state menu hotkeys
Input events ¶
The Python state implementation supports the following callback methods:
Method |
Called for |
|
---|---|---|
|
keyboard events |
See reading the keyboard device below. |
|
keyboard keys transition events |
See reading the keyboard device below. |
|
input device moves/clicks |
See mouse handling below. |
|
mouse double clicks |
See mouse double click handling below. |
|
mouse wheel scroll |
See scroll wheel below. |
The UIEvent object you get from the "ui_event"
key of the dictionary passed to these callbacks has two useful methods: hou.UIEvent.device returns a hou.UIEventDevice object that lets you read the state of the user input device. hou.UIEvent.ray returns a pointing ray into the 3D scene.
Consuming input events ¶
Python states have priority for processing mouse and keyboard events. All input event callbacks can optionally consume the received event by returning True
, this tells Houdini to break the chain of
event consumers and to stop the event processing. Returning False
marks the event as not consumed which is the default behavior.
Warning
Consuming an event may cause some troubles. For instance, if onMouseEvent
returns True to indicate the event has been consumed, the active selector will not receive the mouse event and will break. Events consuming must be done with care.
Reading the UI input device ¶
The hou.UIEventDevice returned by kwargs["ui_event"].device()
in a UI event handler contains methods for getting the screen coordinates of the mouse within the view, the state of the mouse buttons and modifier keys, and also tablet-specific data.
See the help for hou.UIEventDevice to see what data is available.
from __future__ import print_function class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseEvent(self, kwargs): # Get the UI event ui_event = kwargs["ui_event"] # Get the UI device object device = ui_event.device() print("Screen X=", device.mouseX()) print("Screen Y=", device.mouseY()) print("LMB pressed=", device.isLeftButton()) print("MMB pressed=", device.isMiddleButton()) print("RMB pressed=", device.isRightButton()) print("LMB released=", device.isLeftButtonReleased()) print("MMB released=", device.isMiddleButtonReleased()) print("RMB released=", device.isRightButtonReleased()) print("Shift pressed=", device.isShiftKey()) print("Ctrl pressed=", device.isCtrlKey()) print("Alt/option pressed=", device.isAltKey())
Left mouse button ¶
Instead of storing whether the left mouse button was pressed (using hou.UIEventDevice.isLeftButton()) in the last event and comparing it to the current event to see if the mouse was pressed or dragged or released, you can call hou.UIEvent.reason and check the hou.uiEventReason value.
from __future__ import print_function def onMouseEvent(self, kwargs): ui_event = kwargs["ui_event"] reason = ui_event.reason() if reason == hou.uiEventReason.Picked: print("LMB click") elif reason == hou.uiEventReason.Start: print("LMB was pressed down") elif reason == hou.uiEventReason.Active: print("Mouse dragged with LMB down") elif reason == hou.uiEventReason.Changed: print("LMB was released")
Other than mouse clicks/drags, the usual reason returned will be hou.uiEventReason.Located
for a mouse move.
Tip
An alternative to hou.uiEventReason.Changed to see if the left mouse button was released is to call hou.UIEventDevice.isLeftButtonReleased(). To see if the middle button was released call hou.UIEventDevice.isMiddleButtonReleased(). For the right button call hou.UIEventDevice.isRightButtonReleased()
Left mouse button double click ¶
You can trap the double click event with the onMouseDoubleClickEvent
handler. This event is triggered when the left mouse button is depressed 2
times in a quick sequence. The handler is always called after onMouseEvent
has been processed.
Warning
Like onMouseEvent
, onMouseDoubleClickEvent
is always processed before the active selector (if any). If onMouseDoubleClickEvent
doesn’t consume the event (returns False),
the event is passed to the active selector. However, when onMouseDoubleClickEvent
consumes the event (returns True), Houdini will not pass the event to the active
selector and will likely break.
Right mouse button ¶
Mouse wheel ¶
If the user scrolls the mouse wheel vertically, Houdini calls your state’s onMouseWheelEvent()
method (if it exists). You can then read the scroll value from the device using hou.UIEventDevice.mouseWheel.
This API does not currently support scroll axes other than vertical.
from __future__ import print_function class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseWheelEvent(self, kwargs): # Get the UI device object device = kwargs["ui_event"].device() scroll = device.mouseWheel() print("Scroll=", scroll)
See the help for hou.UIEventDevice.mouseWheel for information about the number returned.
Reading the keyboard device ¶
The onKeyEvent
handler let you process keyboard events from single keys to key combinations with modifier keys. The hou.UIEventDevice
returned by kwargs["ui_event"].device()
gives access to the key being pressed and other useful information about the event.
Transitory key events, like up and down key transitions, are monitored with the onKeyTransitEvent
handler. Use this method if you need to know
when a key was released or pressed down. See hou.UIEventDevice.isKeyUp and hou.UIEventDevice.isKeyDown for more.
def onKeyEvent(self, kwargs): ui_event = kwargs['ui_event'] # Log some key event info in the Viewer State Browser console self.log( 'key string', ui_event.device().keyString() ) self.log( 'key value', ui_event.device().keyValue() ) self.log( 'isAutoRepeat', ui_event.device().isAutoRepeat() ) # Use the key string to decide if the event should be consumed or not. # Store the pressed key for use in other handlers. self.key_pressed = ui_event.device().keyString() if self.key_pressed in ('a', 'shift a', 'ctrl g'): # returns True to consume the event return True # Consume the event only if the 'f', 'p' or 'v' key is held if ui_event.device().isAutoRepeat(): ascii_key = ui_event.device().keyValue() if ascii_key in (102, 112, 118): self.key_pressed = ui_event.device().keyString() return True self.key_pressed = None # return False if the event is not consumed return False
def onKeyTransitEvent(self, kwargs): ui_event = kwargs['ui_event'] # Log the key state self.log( 'key', ui_event.device().keyString() ) self.log( 'key up', ui_event.device().isKeyUp() ) self.log( 'key down', ui_event.device().isKeyDown() ) # return True to consume the key transition if ui_event.device().isKeyDown(): return True return False
Consumer chain ¶
The keyboard event consumer chain for python states is as follows
-
onKeyEvent
-
onMenuAction
-
Active state selector
The python state has a first crack at the event with onKeyEvent
, then with onMenuAction
via a symbolic hotkey (if any). Lastly, the active
selector has a turn at consuming the event. onKeyEvent
should return True
if the event is consumed, otherwise the key could be consumed
a second time by onMenuAction
.
Tablet ¶
The hou.UIEventDevice returned by kwargs["ui_event"].device()
in a UI event handler lets you check whether the user is using a tablet, and read tablet-specific intput data.
See the help for hou.UIEventDevice to see what tablet-specific data is available.
from __future__ import print_function class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseEvent(self, kwargs): # Get the UI event ui_event = kwargs["ui_event"] # Get the UI device object device = ui_event.device() print("Screen X=", device.mouseX()) print("Screen Y=", device.mouseY()) print("LMB pressed=", device.isLeftButton()) print("MMB pressed=", device.isMiddleButton()) print("RMB pressed=", device.isRightButton()) print("Shift pressed=", device.isShiftKey()) print("Ctrl pressed=", device.isCtrlKey()) print("Alt/option pressed=", device.isAltKey()) print("Angle=", device.tabletAngle()) print("Pressure=", device.tabletPressure()) print("Roll=", device.tabletRoll()) print("Tilt=", device.tabletTilt())
Getting the pointing ray ¶
-
In the
onMouseEvent
handler, you can get a reference to the hou.ViewerEvent object usingkwargs["ui_event"]
, and then call hou.ViewerEvent.ray to get a world-space origin and direction vector representing a “pointing ray” firing from the mouse pointer “into” the scene. You can use this to tell what’s under the mouse pointer in the 3D scene.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"] origin, direction = ui_event.ray()
-
If you want the pointing ray to take the current snapping controls into account, use hou.ViewerEvent.snappingRay instead of
ray()
. This method returns a dictionary containingorigin_point
,direction
, andsnapped
, where the third item is a boolean indicating whether the ray was snapped. Ifsnapped
isTrue
, then other values will be included in the dictionary which indicate what was snapped to. See Snapping to Geometry for more information.
Intersecting geometry ¶
You can check what geometry is under the pointer using hou.Geometry.intersect. This method is a bit unusual in that it requires setting up C-style “output arguments”. You can make it slightly easier to use by wrapping it in a utility function:
# In viewerstate.utils def sopGeometryIntersection(geometry, ray_origin, ray_dir): # Make objects for the intersect() method to modify position = hou.Vector3() normal = hou.Vector3() uvw = hou.Vector3() # Try intersecting the ray with the geometry intersected = geometry.intersect( ray_origin, ray_dir, position, normal, uvw ) # Returns a tuple of four values: # - the primitive number of the primitive hit, or -1 if the ray didn't hit # - the 3D position of the intersection point (as Vector3) # - the normal of the ray to the hit primitive (as Vector3) # - the uvw coordinates of the intersection on the primitive (as Vector3) return intersected, position, normal, uvw
This function takes a hou.Geometry object, the ray origin, and the ray direction. You get the ray origin and direction from kwargs["ui_event"].ray()
(see above). For the geometry, you should generally use the current node’s input geometry.
Tip
You should get a single reference to the geometry and hold onto it rather than getting the geometry separately in each onMouseEvent()
.
The reason is Houdini builds acceleration structures to make intersection faster when you call Geometry.intersect()
. If you keep one reference to the geometry, you only pay this cost once, whereas if you get a new Geometry
reference in each event, you have to pay this cost over and over, leading to slow performance.
class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer self._geometry = None def onEnter(self, kwargs): node = kwargs["node"] inputs = node.inputs() if inputs and inputs[0]: self._geometry = inputs[0].geometry() def onMouseEvent(self, kwargs): node = kwargs["node"] ui_event = kwargs["ui_event"] ray_origin, ray_dir = ui_event.ray() if self._geometry: hit, pos, norm, uvw = sopGeometryIntersection( self._geometry, ray_origin, ray_dir ) # ...
Depending on the type of state you create, you might want to intersect a different geometry. For example, in an “inspector” state (not tied to a node), you might want to intersect the display geometry:
from stateutils import ancestorObject network = ancestorObject(scene_viewer.pwd()) geometry = network.displayNode().geometry()
The sopGeometryIntersection
function returns a tuple of four items:
Type |
Content |
---|---|
|
An integer representing the primitive number of the primitive hit by the ray. If the ray missed the geometry, this is |
The 3D coordinates of the intersection point. |
|
The direction of the ray relative to the surface of the hit primitive at the intersection point. |
|
The U, V (and W) coordinates of the intersection point across the surface of the hit primitive. |
(If the ray missed, the three vectors are left at their defaults: 0, 0, 0
.)
See flexible intersection below for how to combine geometry intersection with construction plane intersection.
Snapping to Geometry ¶
hou.ViewerEvent.ray is great for finding intersections with geometry or the Houdini construction or reference planes. But if you need to snap a ray position to geometry components then you need hou.ViewerEvent.snappingRay.
hou.ViewerEvent.snappingRay returns a dictionary containing the “pointing
ray” as well as information about the snap, if one occurred. It uses the
snapping options specified in the Snap Options
dialog available in the
toolbar on the left of the Houdini viewer pane. If the Snap Options
are not
enabled, hou.ViewerEvent.snappingRay will not snap to anything.
For example, in an “inspector” state, you might want to check which point the user is hovering near:
def onMouseEvent(self, kwargs): ui_event = kwargs["ui_event"] snap_dict = ui_event.snappingRay() if snap_dict["snapped"] and snap_dict["geo_type"] == hou.snappingPriority.GeoPoint: self.log("You snapped to a point:") self.log(snap_dict["point_index"])
If a state is only interested in a particular type of snapping, it can set the snapping mode of the viewport with hou.SceneViewer.snappingMode. Make sure that the state reverts to the previous snapping mode when it is done:
def onEnter(self, kwargs): self._snap_mode = self.scene_viewer.snappingMode() self.scene_viewer.setSnappingMode(hou.snappingMode.Point) def onInterrupt(self, kwargs): self.scene_viewer.setSnappingMode(self._snap_mode) def onResume(self, kwargs): self._snap_mode = self.scene_viewer.snappingMode() self.scene_viewer.setSnappingMode(hou.snappingMode.Point) def onExit(self, kwargs): self.scene_viewer.setSnappingMode(self._snap_mode)
Interacting with the intersected primitive ¶
-
If the first number returned by
sopGeometryIntersection()
is not-1
, it is a primitive number. You can get a hou.Prim object for that primitive using hou.Geometry.prim. -
The object you get will often be a more specialized subclass of
Prim
. For example, if you ask for a polygon primitive, you will get a hou.Polygon object.The best way to check what kind of primitive you have is to call hou.Prim.type and check what kind of hou.primType value it returns.
prim = geometry.prim(prim_num) if prim.type() != hou.primType.Polygon: raise hou.Error("This tool only works with polygons")
-
The hou.Prim/hou.Polygon object has lots of useful methods for inspecting the geometry. (Remember that the geometry you get from the scene is read-only.)
For example:
-
Get the bounding box of the primitive (hou.Prim.boundingBox).
-
Get the value of a primitive attribute (hou.Prim.attribValue).
-
Get references to the polygon’s vertices (hou.Prim.vertices).
-
Use the
uvw
coordinates returned bysopGeometryIntersection()
to look up the blended value of a point attribute at that location on the polygon surface (hou.Prim.attribValueAtInterior).
-
-
Note that some primitive, point, or vertex related methods might be on the hou.Geometry object rather than on hou.Prim, hou.Point, or hou.Vertex.
For example, if you want a list of all current primitive attributes, that information is actually on the
Geometry
object (hou.Geometry.primAttribs).
Intersecting a plane ¶
The hou.hmath.intersectPlane function lets you find the intersection position between a ray and an arbitrary plane.
The intersectPlane
function expects the plane in the form of an origin and a normal vector. Often you will want to project the pointing ray onto the construction plane or reference plane. You can get the position and orientation of the construction or reference plane as a transformation matrix, but you will have to do some conversion to turn that into an origin and normal. The following function shows how to do the conversion and get the intersection with a plane object.
# In stateutils def cplaneIntersection(scene_viewer, ray_origin, ray_dir): # Find the intersection between the pointing ray and the construction # plane. Returns a hou.Vector3 representing the point in world space, # or raises an exception if the ray does not hit the plane # Grab a reference to the construction plane cplane = scene_viewer.constructionPlane() # Get the construction plane's transform matrix xform = cplane.transform() # type: hou.Matrix4 # The "rest" position for the construction plane has origin=0, 0, 0 # and normal=0, 0, 1. We can multiply the current transform matrix by # those vectors to get the current origin and normal. cplane_origin = hou.Vector3(0, 0, 0) * xform # type: hou.Vector3 cplane_normal = hou.Vector3(0, 0, 1) * xform.inverted().transposed() # Use convenience function in hmath to find intersection return hou.hmath.intersectPlane( cplane_origin, cplane_normal, ray_origin, ray_dir ) # Grab a reference to a scene viewer's construction plane. # Note you could do scene_viewer.referencePlane() to get the # reference plane instead. cplane = scene_viewer.constructionPlane() # Given a ray origin and direction, computer the intersection # point with the construction plane point = cplane_intersection(cplane, origin, direction)
Flexible intersection ¶
For some tools, you might want to project onto geometry if it’s available, or onto the construction plane if the pointing ray doesn’t hit any geometry. For example:
from __future__ import print_function from stateutils import sopGeometryIntersection class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer self._geometry = None def onEnter(self, kwargs): node = kwargs["node"] inputs = node.inputs() if inputs and inputs[0]: self._geometry = inputs[0].geometry() def onMouseEvent(self, kwargs): node = kwargs["node"] ui_event = kwargs["ui_event"] device = ui_event.device() origin, direction = ui_event.ray() intersected = -1 inputs = node.inputs() if inputs and inputs[0]: # Only try intersecting geometry if the node has input intersected, position = sopGeometryIntersection( self._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) print("position=", position)
Compensating for parent transforms ¶
If you want to display Drawable guide geometry based on the ray (for example, display a “cursor” guide in the scene showing the intersection position), you should transform local coordinates into world space.
See compensating guide transforms for more information.
Interrupt and Resume events ¶
Method name |
Called by Houdini |
Notes |
---|---|---|
|
state is interrupted |
This method is called when:
|
|
interruption ends |
This method is called when:
|
-
If you're remembering and comparing the state of mouse buttons in the
onMouseEvent()
handler to tell if the mouse button is being held down, you should also implementonInterrupt()
and make it act like the user releasing the mouse button.Note that, for the left mouse button, you don’t need to track its state manually, you can use the UIEvent reason.
-
If you want to show something when the mouse pointer is in the viewer (for example a guide geometry preview under the mouse pointer), and hide it when the user is doing something else, you should hide in
onInterrupt()
but show inonMouseEvent()
instead ofonResume()
. The state is active and the mouse pointer is in the viewer, by definition, whenonMouseEvent()
is called, and usingonMouseEvent()
instead ofonResume()
allows you to update what you're showing based on the mouse position if needed.