On this page |
概要 ¶
(独自のViewer Stateを実装する基本的な方法は、Pythonステートを参照してください。)
ノードがユーザ操作できるようにする標準的な方法は、ノードのステートでノードパラメータにハンドルをバインドさせることです。 ハンドル自体が非常に強力なので、既に用意されているユーザインターフェースを指定するだけでも幅広く色々とパラメータをセットアップすることができます。
しかし、場合によっては、マウスを動かした時、ボタンが押された時、マウスホイールをクリックした時、タブレットのペンを傾斜させたり圧力を加えた時といったローレベルのユーザ入力を解釈する機能を追加したいことがあります。
onMouseEvent
コールバックを実装することで、ローレベルのイベントを制御することができます。
-
ステートがアクティブになっている状態だと、マウスイベントハンドラーによって、そのマウスからスクリーン“に向かった” 光線 (方向ライン)を取得することができます。このポインティング光線とジオメトリの交差を判定することで、そのマウスポインタ下にジオメトリがあるのかどうかを調べることができます。
-
カスタムステートで右クリックのコンテキストメニューをセットアップしたいのであれば、Pythonステートメニューを参照してください。
-
UIデバイスオブジェクトには、修飾キーや矢印キーを検出するための便利なメソッドが含まれています。ホットキーをキャプチャーする方法に関しては、Pythonステートメニューホットキーを参照してください。
入力イベント ¶
Pythonステートの実装は、以下のコールバックメソッドに対応しています:
メソッド |
コールバックされるタイミング |
|
---|---|---|
|
キーボードイベント |
以下のキーボードデバイスを読み込む方法を参照してください。 |
|
キーボードキーの遷移イベント |
以下のキーボードデバイスを読み込む方法を参照してください。 |
|
入力デバイスが移動/クリックされた時 |
以下のマウスの制御を参照してください。 |
|
マウスがダブルクリックされた時 |
以下のマウスのダブルクリック制御を参照してください。 |
|
マウスホイールがスクロールされた時 |
以下のホイールのスクロールを参照してください。 |
これらのコールバックに渡される辞書のui_event
キーから取得したUIEventオブジェクトには、2つの便利なメソッドがあります: hou.UIEvent.deviceは、ユーザ入力デバイスのステートを読み込むことができるhou.UIEventDeviceオブジェクトを返します。hou.UIEvent.rayは、3Dシーン内のポインティング光線を返します。
入力イベントを消費する方法 ¶
Pythonステートには、マウスイベントとキーボードイベントを処理する上で優先順位があります。
どの入力イベントコールバックも、必要に応じてTrue
を返すことで、受信したイベントを消費することができます。
True
を返すと、Houdiniはイベント消費チェーンを断ち切ってイベント処理を停止します。
False
を返すと、そのイベントを消費しないものとしてマークされます。これがデフォルトの挙動です。
Warning
イベントを消費すると、いくつかのトラブルが発生する可能性があります。
例えば、イベントが消費されたことを示すためにonMouseEvent
がTrueを返すと、アクティブなセレクターがマウスイベントを受信しなくなって壊れます。
イベントを消費する際は必ず注意して処理してください。
UI入力デバイスを読み込む方法 ¶
UIイベントハンドラーのkwargs["ui_event"].device()
が返すhou.UIEventDeviceには、ビューア内でのマウスのスクリーン座標、マウスボタンと修飾キーの状態、さらにはタブレット固有のデータを取得するメソッドが含まれています。
利用可能なデータに関しては、hou.UIEventDeviceのヘルプを参照してください。
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): # UIイベントを取得します。 ui_event = kwargs["ui_event"] # UIデバイスオブジェクトを取得します。 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())
左マウスボタン ¶
最後のイベントで左マウスボタンが押されてのかどうか(hou.UIEventDevice.isLeftButton()を使用)を格納するのではなく、最後のイベントと現行イベントを比較することで、マウスが押されたのかドラッグされたのかリリースされたのか調べたいのであれば、 hou.UIEvent.reasonをコールして、そのhou.uiEventReason値をチェックしてください。
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")
マウスのクリック/ドラッグ以外にもよく使用する戻り値は、マウスの移動をチェックするためのhou.uiEventReason.Located
です。
Tip
左マウスボタンが離されたかどうか確認するhou.uiEventReason.Changedの代替の方法は、hou.UIEventDevice.isLeftButtonReleased()をコールすることです。 中マウスボタンが離されたかどうか確認したいのであれば、hou.UIEventDevice.isMiddleButtonReleased()をコールします。 右マウスボタンが離されたかどうか確認したいのであれば、hou.UIEventDevice.isRightButtonReleased()をコールします。
左マウスボタンのダブルクリック ¶
onMouseDoubleClickEvent
ハンドラーを使用してダブルクリックイベントをトラップにかけることができます。
このイベントは、左マウスボタンが連続で2回押された時に発動されます。
このハンドラーは、onMouseEvent
が処理された後に常にコールされます。
Warning
onMouseEvent
と同様に、onMouseDoubleClickEvent
は常に(存在すれば)アクティブセレクターの前に処理されます。
onMouseDoubleClickEvent
がイベントを消費しない(Falseを返す)場合、そのイベントはアクティブセレクターに渡されます。
しかし、onMouseDoubleClickEvent
がイベントを消費する(Trueを返す)場合、Houdiniはそのイベントをアクティブセレクターに渡さなくなって壊れる可能性があります。
右マウスボタン ¶
Pythonステートがカスタムメニューをバインドしていない場合、右マウスボタン(RMB)のイベントはonMouseEvent
で処理されません。
Pythonステートがカスタムメニューをバインドしていない場合、以下の条件がすべて満たされると、 RMB イベントがonMouseEvent
に送信されます:
-
ビューポート内に何もアクティブなハンドルがない。または、アクティブなハンドルが1つあったとしても RMB イベントがそのアクティブハンドルメニューを発動しなかった。
-
アクティブなセレクターがない。
-
ビューポート内にジオメトリもオブジェクトも存在しない。または、1つあったとしても RMB イベントがそのジオメトリまたはオブジェクトのメニューを発動しなかった。
def onMouseEvent(self, kwargs): ui_event = kwargs["ui_event"] if ui_event.device().isLeftButton(): self.log("LMB click") elif ui_event.device().isMiddleButton(): self.log("MMB click") elif ui_event.device().isRightButton(): self.log("RMB click") return True return False
マウスホイール ¶
ユーザがマウスホイールを垂直にスクロールさせると、Houdiniは、あなたのステートからonMouseWheelEvent()
メソッド(存在していれば)をコールします。
hou.UIEventDevice.mouseWheelを使用することで、デバイスからスクロール値を読み込むことができます。
現在のところ、このAPIは垂直以外のスクロール軸には対応していません。
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): # UIデバイスオブジェクトを取得します。 device = kwargs["ui_event"].device() scroll = device.mouseWheel() print("Scroll=", scroll)
戻り値の数値に関する情報は、hou.UIEventDevice.mouseWheelのヘルプを参照してください。
キーボードデバイスの読み込み方 ¶
onKeyEvent
ハンドラーは、単一キーから修飾キーとのキーの組み合わせまでのキーボードイベントを処理することができます。
kwargs["ui_event"].device()
が返したhou.UIEventDeviceは、押されているキー、そのイベントに関する他に役立つ情報にアクセスすることができます。
上下キーの遷移のような遷移キーイベントは、onKeyTransitEvent
ハンドラーを使って監視します。
キーが離されたまたは押されたタイミングを知る必要がある場合は、このメソッドを使用します。
詳細は、hou.UIEventDevice.isKeyUpとhou.UIEventDevice.isKeyDownを参照してください。
def onKeyEvent(self, kwargs): ui_event = kwargs['ui_event'] # Viewer State Browserコンソール内に何かしらのキーイベント情報のログを残します。 self.log( 'key string', ui_event.device().keyString() ) self.log( 'key value', ui_event.device().keyValue() ) self.log( 'isAutoRepeat', ui_event.device().isAutoRepeat() ) # キー文字列を使って、キーイベントを実行するかどうかを決定します。 # 他のハンドラーで実行できるように、押されたキーを格納します。 self.key_pressed = ui_event.device().keyString() if self.key_pressed in ('a', 'shift a', 'ctrl g'): # キーイベントを実行するためにTrueを返します。 return True # 'f', 'p', 'v'のキーが押されている場合にのみ、キーイベントを実行します。 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 # キーイベントを実行しない場合はFalseを返します。 return False
def onKeyTransitEvent(self, kwargs): ui_event = kwargs['ui_event'] # キーの状態をログに記録します。 self.log( 'key', ui_event.device().keyString() ) self.log( 'key up', ui_event.device().isKeyUp() ) self.log( 'key down', ui_event.device().isKeyDown() ) # キーの遷移を消費するためにTrueを返します。 if ui_event.device().isKeyDown(): return True return False
消費チェーン ¶
Pythonステートに関するキーボードイベントの消費チェーンは以下のとおりです
-
onKeyEvent
-
onMenuAction
-
アクティブステートセレクター
Pythonステートは、まず最初にonKeyEvent
でイベントを発生させ、次に(あれば)シンボリックホットキーを介してonMenuAction
でイベントを発生させます。
最後に、アクティブセレクターはイベントを消費する順番を持ちます。
イベントが消費されるとonKeyEvent
はTrue
を返し、そうでなければ、次にonMenuAction
でそのキーを消費することができます。
タブレット ¶
UIイベントハンドラーのkwargs["ui_event"].device()
が返すhou.UIEventDeviceは、ユーザがタブレットを使用しているかどうかチェックしたり、タブレット固有の入力データを読み込むことができます。
利用可能なタブレット固有のデータに関しては、hou.UIEventDeviceのヘルプを参照してください。
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): # UIイベントを取得します。 ui_event = kwargs["ui_event"] # UIデバイスオブジェクトを取得します。 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())
ポインティング光線を取得する方法 ¶
-
onMouseEvent
ハンドラーでは、kwargs["ui_event"]
を使ってhou.ViewerEventオブジェクトの参照を取得してから、hou.ViewerEvent.rayをコールすることで、マウスポイントからシーン“へ”向かった“ポインティング光線”を表現したワールド空間の原点とその方向ベクトルを取得することができます。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()
-
現在のスナップコントロールを考慮した ポインティング光線を取得したいのであれば、
ray()
の代わりにhou.ViewerEvent.snappingRayを使用してください。 このメソッドは、origin_point
,direction
,snapped
を含んだ辞書を返します。 3番目の項目は、光線がスナップされたかどうかを示すブール値です。snapped
がTrue
の場合、どこにスナップしたのかを示した他の値が辞書に含まれます。 詳細は、ジオメトリへのスナップを参照してください。
ジオメトリの交差 ¶
hou.Geometry.intersectを使用することで、マウスポインタ下にジオメトリがあるのかどうかをチェックすることができます。 このメソッドは、C言語スタイルで“出力引数”をセットアップする必要があるという点では少し特殊です。ユーティリティ関数でそれをラップすることで、それを少し使いやすくすることができます:
# viewerstate.utils内 def sopGeometryIntersection(geometry, ray_origin, ray_dir): # intersect()メソッドで修正するオブジェクトを作成します。 position = hou.Vector3() normal = hou.Vector3() uvw = hou.Vector3() # 光線とジオメトリの交差を試します。 intersected = geometry.intersect( ray_origin, ray_dir, position, normal, uvw ) # 4つの値のタプルを返します: # - 当たったプリミティブのプリミティブ番号、光線が当たらなかった場合は-1。 # - 交点の3D位置(Vector3) # - 光線が当たったプリミティブの法線(Vector3) # - プリミティブ上の交点のuvw座標(Vector3) return intersected, position, normal, uvw
この関数は、hou.Geometryオブジェクト、光線の原点、光線の方向を受け取ります。
kwargs["ui_event"].ray()
から光線の原点と方向を取得することができます(上記を参照)。
ジオメトリに関しては、通常では 現行ノードの入力ジオメトリ を使用してください。
Tip
onMouseEvent()
の度に別々にジオメトリを取得するのではなくて、 ジオメトリの参照を1個取得したら、それを維持すべきです 。
その理由は、Geometry.intersect()
をコールすると、Houdiniは交差を高速化するために加速化構造を構築するからです。ジオメトリの参照を1つだけ維持していれば、一度だけこの負荷を負うだけで済みますが、
イベントの度に新しくGeometry
参照を取得していると、その度にこの負荷を負わなければならなくなって、パフォーマンスが悪くなってしまいます。
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 ) # ...
作成するステートのタイプに応じて、異なるジオメトリと交差させたいことがあります。 例えば、(ノードと関係のない)“検査”タイプのステートは、ディスプレイジオメトリと交差させたいです:
from stateutils import ancestorObject network = ancestorObject(scene_viewer.pwd()) geometry = network.displayNode().geometry()
sopGeometryIntersection
関数は、4個のアイテムのタプルを返します:
タイプ |
内容 |
---|---|
|
光線が当たったプリミティブのプリミティブ番号を表現した整数。光線がジオメトリに当たらなかった場合、これは |
交点の3D座標。 |
|
当たったプリミティブの表面を基準とした交点における光線の方向。 |
|
当たったプリミティブの表面上の交点のU, V(とW)座標。 |
(光線が当たらなかった場合、その値はデフォルトのままです: 0, 0, 0
。)
ジオメトリ交差とConstruction Planeの交差を組み合わせる方法については、以下の柔軟な交差を参照してください。
ジオメトリへのスナップ ¶
hou.ViewerEvent.rayは、ジオメトリやHoudiniのコンストラクション平面/参照平面との交点を検索するのに非常に良いです。 しかし、光線位置をジオメトリコンポーネントにスナップさせる必要がある場合は、hou.ViewerEvent.snappingRayが必要になります。
hou.ViewerEvent.snappingRayは、“ポインティング光線”だけでなく、スナップが発生した際のそのスナップに関する情報も含んだ辞書を返します。
このイベントは、Houdini Viewerペインの左側にあるツールバーで利用可能なSnap Options
ダイアログで指定されているスナップオプションを使用します。
Snap Options
が有効になっていない場合は、hou.ViewerEvent.snappingRayはどこにもスナップしません。
“Inspector”ステートを例にすると、マウスカーソルの位置がどの付近にあるのかチェックしたいです:
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"])
ステートが特定のタイプのスナップでしか交差しないようにする場合、hou.SceneViewer.snappingModeを使って、ビューポートのスナップモードを設定することができます。 これを行なった時は、必ずステートが前のスナップモードに戻すようにしてください:
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)
交差したプリミティブを扱う方法 ¶
-
sopGeometryIntersection()
が返す最初の番号が-1
でない限り、それはプリミティブ番号です。hou.Geometry.primを使用することで、そのプリミティブのhou.Primオブジェクトを取得することができます。 -
得られるオブジェクトは、
Prim
をもっと特別にした サブクラス であることが多いです。例えば、ポリゴンプリミティブと交差した場合は、hou.Polygonオブジェクトが取得されます。取得したプリミティブの種類を確認する最も良い方法は、hou.Prim.typeをコールして、それが返したhou.primType値の種類を確認することです。
prim = geometry.prim(prim_num) if prim.type() != hou.primType.Polygon: raise hou.Error("This tool only works with polygons")
-
hou.Prim/hou.Polygonオブジェクトには、ジオメトリを検査するための便利なメソッドがたくさんあります(シーンから取得したジオメトリは読み込み専用であることを忘れないでください)。
例:
-
プリミティブの境界ボックスを取得する(hou.Prim.boundingBox)。
-
Primitiveアトリビュートの値を取得する(hou.Prim.attribValue)。
-
ポリゴンの頂点の参照を取得する(hou.Prim.vertices)。
-
sopGeometryIntersection()
が返すuvw
座標を使用することで、ポリゴンサーフェス(hou.Prim.attribValueAtInterior)上のその位置におけるブレンドされたPointアトリビュートの値を照会することができます。
-
-
プリミティブ/ポイント/頂点関連の一部のメソッドは、hou.Prim, hou.Point, hou.Vertexではなく、hou.Geometryオブジェクトにあることに注意してください。
例えば、現行のPrimitiveアトリビュートすべてのリストを取得したいのであれば、その情報は実際には
Geometry
オブジェクト(hou.Geometry.primAttribs)上にあります。
平面と交差させる方法 ¶
hou.hmath.intersectPlane関数は、光線と任意の平面の交点を求めることができます。
intersectPlane
関数には、原点と法線ベクトルで平面を指定する必要があります。
ポインティング光線をConstruction PlaneまたはReference Plane上に投影したいこともよくあります。
Construction PlaneまたはReference Planeの位置と向きをトランスフォームマトリックスとして取得することができますが、
その場合には、何かしらの変換処理によって、それを原点と法線に変換しなければなりません。
以下の関数は、その変換と平面オブジェクトとの交点を求める方法について説明しています。
# stateutils内 def cplaneIntersection(scene_viewer, ray_origin, ray_dir): # ポインティング光線とConstruction Planeの交点を求めます。 # ワールド空間での交点を表現したhou.Vector3を返します。 # 光線が平面に当たらなかった場合には例外を引き起こします。 # Construction Planeの参照を取得します。 cplane = scene_viewer.constructionPlane() # Construction Planeのトランスフォームマトリックスを取得します。 xform = cplane.transform() # type: hou.Matrix4 # Construction Planeの"rest(静止)"ポジションの原点は、0, 0, 0、法線は0, 0, 1です。 # それらのベクトルを現行トランスフォームマトリックスに乗算することで、 # 現行平面の原点と法線を取得することができます。 cplane_origin = hou.Vector3(0, 0, 0) * xform # type: hou.Vector3 cplane_normal = hou.Vector3(0, 0, 1) * xform.inverted().transposed() # hmathの便利関数を使用して、交点を求めます。 return hou.hmath.intersectPlane( cplane_origin, cplane_normal, ray_origin, ray_dir ) # シーンビューアのConstruction Planeの参照を取得します。 # 他にも、scene_viewer.referencePlane()によって参照平面を取得することもできます。 cplane = scene_viewer.constructionPlane() # 光線の原点と方向を指定すると、そのConstruction Planeとの交点が計算されます。 point = cplane_intersection(cplane, origin, direction)
柔軟な交差 ¶
一部のツールでは、光線をジオメトリ上に投影したり、(何のジオメトリにも当たらなかった場合には)Construction Plane上に投影することができます。例:
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]: # ノードに入力があれば、ジオメトリとの交差のみを試します。 intersected, position = sopGeometryIntersection( self._geometry, origin, direction ) if intersected < 0: # 入力ジオメトリがなかった場合、または光線が当たらなかった場合、 # コンストラクション平面との交差を試します。 position = cplaneIntersection(self.scene_viewer, origin, direction) print("position=", position)
親トランスフォームの補正 ¶
光線に基づいてDrawableガイドジオメトリを表示したい場合(例えば、交点を示せるようにシーン内に“カーソル”のガイドを表示したい場合)、ローカル座標をワールド空間に変換してください。
詳細は、ガイドトランスフォームを補正する方法を参照してください。
中断/再開のイベント ¶
メソッド名 |
コールされるタイミング |
メモ |
---|---|---|
|
ステートが中断された時 |
このメソッドは以下の時にコールされます:
|
|
中断を終了した時 |
このメソッドは以下の時にコールされます:
|
-
onMouseEvent()
ハンドラーでマウスボタンの状態を覚えて比較することで、マウスボタンが押されたままになっているか伝えるようにした場合、さらにonInterrupt()
には、ユーザがマウスボタンを離した時の挙動も実装してください。左マウスボタンに関しては、手動でステートを追跡する必要はなくて、この理由からUIEventを使用してください。
-
マウスポインタがビューア内にある時に何かを表示させたい場合(例えば、マウスポインタ下にガイドジオメトリをプレビューさせたい場合)や、ユーザが他の処理をしている時にそれを非表示にしたい場合には、
onInterrupt()
でガイドを非表示にし、onResume()
の代わりにonMouseEvent()
でガイドを表示してください。onMouseEvent()
をコールした時に、定義によってはステートがアクティブでマウスポインタがビューア内にあった場合、onResume()
の代わりにonMouseEvent()
を使用することで、必要に応じてマウスの位置から表示すべき内容を更新することができます。