Houdini 17.0 Pythonスクリプト

Pythonステート ユーザーインターフェースイベント

直接的なUI入力を検知して反応させる方法。

On this page

Pythonビューアステート

概要

(独自のViewer Stateを実装する基本的な方法は、Pythonステートを参照してください。)

ノードがユーザー操作できるようにする標準的な方法は、ノードのステートでノードパラメータにハンドルをバインドさせることです。 ハンドル自体が非常に強力なので、既に用意されているユーザーインターフェースを指定するだけでも幅広く色々とパラメータをセットアップすることができます。

しかし、場合によっては、マウスを動かした時、ボタンが押された時、マウスホイールをクリックした時、タブレットのペンを傾斜させたり圧力を加えた時といったローレベルのユーザー入力を解釈する機能を追加したいことがあります。 これは、onMouseEvent()コールバックの実装で可能です。

  • ステートがアクティブになっている状態だと、マウスイベントハンドラーによって、そのマウスからスクリーン"に向かった" 光線 (方向ライン)を取得することができます。このポインティング光線とジオメトリの交差を判定することで、そのマウスポインタ下にジオメトリがあるのかどうかを調べることができます。

  • カスタムステートで右クリックのコンテキストメニューをセットアップしたいのであれば、Pythonステートメニューを参照してください。

  • UIデバイスオブジェクトには、修飾キーや矢印キーを検出するための便利なメソッドが含まれています。ホットキーをキャプチャーする方法に関しては、Pythonステートメニューホットキーを参照してください。

入力イベント

Pythonステートの実装は、以下のコールバックメソッドに対応しています:

メソッド

コールバックされるタイミング

onMouseEvent

入力デバイスが移動/クリックされた時

以下のUIデバイスを読み込む方法を参照してください。

onMouseWheelEvent

マウスホイールがスクロールされた時

以下のホイールのスクロールを参照してください。

これらのコールバックに渡される辞書のui_eventキーから取得したUIEventオブジェクトには、2つの便利なメソッドがあります: hou.UIEvent.device()は、ユーザー入力デバイスのステートを読み込むことができるhou.UIEventDeviceオブジェクトを返します。hou.UIEvent.rayは、3Dシーン内のポインティング光線を返します。

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 onMouseWheelEvent(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())

左マウスボタン

最後のイベントで左マウスボタンが押されてのかどうか(device.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です。

マウスホイール

ユーザーがマウスホイールを垂直にスクロールさせると、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()のヘルプを参照してください。

タブレット

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 onMouseWheelEvent(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())

        if device.isTablet():
            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()を使用してください。このメソッドは、(ray_origin, ray_direction, snapped)のタプルを返します。3番目の項目は、光線がスナップされたかどうかを示すブール値です。

ジオメトリの交差

hou.Geometry.intersect()を使用することで、マウスポインタ下にジオメトリがあるのかどうかをチェックすることができます。 このメソッドは、C言語スタイルで"出力引数"をセットアップする必要があるという点では少し特殊です。ユーティリティ関数でそれをラップすることで、それを少し使いやすくすることができます:

# stateutils内
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"]
        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個のアイテムのタプルを返します:

タイプ

内容

int

光線が当たったプリミティブのプリミティブ番号を表現した整数。光線がジオメトリに当たらなかった場合、これは-1です。

hou.Vector3

交点の3D座標。

hou.Vector3

当たったプリミティブの表面を基準とした交点における光線の方向。

hou.Vector3

当たったプリミティブの表面上の交点のU, V(とW)座標。

(光線が当たらなかった場合、その値はデフォルトのままです: 0, 0, 0。)

ジオメトリ交差とConstruction Planeの交差を組み合わせる方法については、以下の柔軟な交差を参照してください。

交差したプリミティブを扱う方法

  • 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:
            # 入力ジオメトリがなかった場合、または光線が当たらなかった場合、
            # Construction Planeとの交差を試します。
            position = cplaneIntersection(self.scene_viewer, origin, direction)

        print("position=", position)

親トランスフォームの補正

光線に基づいてDrawableガイドジオメトリを表示したい場合(例えば、交点を示せるようにシーン内に"カーソル"のガイドを表示したい場合)、ローカル座標をワールド空間に変換してください。

詳細は、ガイドトランスフォームを補正する方法を参照してください。

中断/再開のイベント

メソッド名

コールされるタイミング

メモ

onInterrupt

ステートが中断された時

このメソッドは以下の時にコールされます:

  • ウィンドウがフォーカスを失なった時。

  • ポインタがビューアから抜けた時(メニューの上に移動した時も含む)。

  • ユーザーが"Volatile"ツール(例えば、((S)を押したままにすると表示されるツール)を押してVolatile選択ツールに切り替えた時)。

onResume

中断を終了した時

このメソッドは以下の時にコールされます:

  • ポインタが再度ビューアに戻った時。

  • ユーザーが"Volatile"ツールからこのステートに戻った時(例えば、Volatile選択ツールを使った後にSを離した時)。

  • onMouseEvent()ハンドラーでマウスボタンの状態を覚えて比較することで、マウスボタンが押されたままになっているか伝えるようにした場合、さらにonInterrupt()には、ユーザーがマウスボタンを離した時の挙動も実装してください。

    左マウスボタンに関しては、手動でステートを追跡する必要はなくて、この理由からUIEventを使用してください。

  • マウスポインタがビューア内にある時に何かを表示させたい場合(例えば、マウスポインタ下にガイドジオメトリをプレビューさせたい場合)や、ユーザーが他の処理をしている時にそれを非表示にしたい場合には、onInterrupt()でガイドを非表示にし、onResume()の代わりにonMouseEvent()でガイドを表示してください。onMouseEvent()をコールした時に、定義によってはステートがアクティブでマウスポインタがビューア内にあった場合、onResume()の代わりにonMouseEvent()を使用することで、必要に応じてマウスの位置から表示すべき内容を更新することができます。

Pythonビューアステート

Pythonスクリプト

はじめよう

次のステップ

Pythonビューアステート

Pythonでビューアステートを記述することで、ビューポート内でノードのユーザー操作をカスタマイズすることができます。

導師レベル

リファレンス

  • hou

    Houdiniにアクセスできるサブモジュール、クラス、ファンクションを含んだモジュール。

  • Alembic拡張関数

    Alembicファイルから情報を抽出するための便利な関数です。