Houdini 20.0 Pythonスクリプト

Pythonステート ガイドジオメトリ

独自ステートのデータとユーザ操作に基づいてビューポート内でガイドジオメトリを表示する方法。

On this page

概要

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

ガイドジオメトリ とは、ステートがアクティブになっている間でのみ表示される3Dの“ユーザインターフェース”ジオメトリのことです。 つまり、SOPネットワークから生成される“実際の”ジオメトリとは異なります。 “Wind Force”ノードを例にすると、そのガイドジオメトリは、風のフォースの方向と強度を示した矢印がそれです。 Brushノードを例にすると、そのガイドジオメトリは、そのブラシのジオメトリに対する影響領域を示したリングがそれです。

ステートには、ガイドジオメトリを持たせないようにすることもできるし、Verbを使って生成可能な単純なジオメトリを表示するようにすることもできるし、アセット内でジオメトリをクックして精巧なガイドとプレビューを表示させることもできます。

Drawableオブジェクトとジオメトリオブジェクト

Houdiniは、ガイドジオメトリを表示させることができるDrawableオブジェクトをいくつか備えています。 ジオメトリソースには、hou.Geometryオブジェクトまたはhou.drawablePrimitiveの値を指定することができます。

このAPIを使って学習したいのであれば、Drawableオブジェクトのヘルプを参照してください。

  • Drawableオブジェクトは、ジオメトリオブジェクトの参照を維持します。大元のジオメトリオブジェクトの内容が変わっても、Drawableオブジェクトを再生成させる必要はなくて、自動的にその変更が反映されます。

  • hou.SopNode.geometryを使ってSOPノードからジオメトリを取得した場合、または、hou.SimpleDrawable.geometryhou.GeometryDrawable.geometryを使ってDrawableオブジェクトからジオメトリを取得した場合、 その結果のジオメトリオブジェクトは、そのノードの出力のライヴの読み込み専用の参照になっています。 このノードの出力が変わった場合(例えば、その出力がアセット上のパラメータで駆動されている場合)、そのジオメトリオブジェクトの内容も自動的に更新されます。

  • ガイドジオメトリは、“実際の”ジオメトリと区別できるようにhou.SimpleDrawableを使ってワイヤーフレームで描画することが多いです。 このワイヤーフレーム表示は、Simple Drawableオブジェクトではデフォルトの表示モードです。 hou.SimpleDrawable.setWireframeColorを使用することで、そのワイヤーフレームのカラーを設定することができます。 hou.SimpleDrawable.setDisplayModeを使用することで、そのDrawableオブジェクトをシェーディングモードに変更することができます。

  • hou.GeometryDrawableを使って作成されたガイドジオメトリは、デフォルトではワイヤーフレームで表示されません。 このAPIには、ジオメトリの表示に関する色々なオプションが備わっています。

  • Drawableオブジェクトは常にワールド空間で描画されます。ローカル座標をワールド空間に変換する方法は、以下のガイドトランスフォームの補正を参照してください。

  • ガイドジオメトリをアニメーションさせたいのであれば、Drawableオブジェクトをフレーム毎に再生成させるよりも、Drawableオブジェクトのトランスフォームを制御した方が非常に効率的です。以下のガイドの位置変更、回転、スケールを参照してください。

    例えば、地面上のマウス位置に球を追従させたい場合、マウスを動かす度に異なる位置に球で新しいDrawableオブジェクトを生成するのではなくて、一度球を作成してから、その球のトランスフォームを変更させて動かしてください。

  • Drawableオブジェクトの参照は、ビューア内で表示を継続させるためには、そこから抜けなければなりません。それがPythonガーベジコレクターによって削除されないようにするには、ステートを実装したオブジェクトのDrawableオブジェクトに対して参照を格納してください。

  • Drawableオブジェクトは、最初にそのオブジェクトを表示した瞬間に消えてしまうことがあります。ビューアが再描画された時、例えば、ユーザがタンブルした時に再度表示されます。hou.GeometryViewport.drawを使用することで、それぞれのビューポートを強制的に再描画させることができます:

    scene_viewer.curViewport().draw()
    

ガジェット

Pythonステートのガジェットは、マウス下のジオメトリのピック(選択)やロケート(マウス下の検索)を視覚的にできるように設計された特別なジオメトリDrawableです。 このガジェットはPythonステートで色々な方法で使用することができます。 例えば、ガジェットを使用することで、ガイドジオメトリを改良したり、本格的にPythonハンドルを組む必要性もないような場合の独自のプライベートカスタムハンドルを作成することができます。

Pythonステートのガジェットの使い方はPythonハンドルのガジェットと同様で、利用できるようにするにはまず最初にガジェットを登録する必要があります。 登録されたガジェットのインスタンスは、自動的にHoudiniによって生成されて、そのガジェット名をキーとした辞書アトリビュート(state_gadgets)としてPythonステートクラスに割り当てられます。

以下のコードスニペットでは、Pythonステートでのガジェットの使い方を載せています。 onMousEventでは、self.state_contextを使用することでユーザ操作を制御しています。 ステート毎に1個のコンテキストがあり、そのコンテキストはHoudiniによって生成されるので、このタイプのオブジェクトをあなた自身で作成する必要はありません。 self.state_contextには、ロケートまたはピックしたガジェットのコンテキスト情報が格納されます。 onMouseEventは単にactiveなガジェットの名前とそのコンポーネント識別子を使ってマウスカーソルを更新しているだけです。

onDrawは単にカーソル下にあるジオメトリポリゴンでフェースガジェットDrawableを更新しているだけです。 self.state_contextは既にHoudiniによってポリゴンIDで更新されているので、Pythonステートはジオメトリ交差を実行してポリゴンをロケート(マウス下の検索)する必要はありません。

def createViewerStateTemplate():
    """登録するViewerステートテンプレートを作成して返すための必須のエントリーポイント。"""

    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):
        """ノード入力ジオメトリをガジェットに割り当てます。
        """
        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):
        """カーソルのテキスト位置とDrawableジオメトリを計算します。
        """
        # 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 ):
        """このコールバックはDrawableのレンダリングに使用されます。
        """
        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)

以下のもっと複雑なコードスニペットでは、ロケートしたポリゴン法線に沿ってジオメトリを移動できるようにhou.ViewerStateDraggerによるガジェットの使い方を説明しています。 hou.ViewerStateDraggerの使い方は、ここで説明されているとおりhou.ViewerHandleDraggerと同様です。

完全なデモは$HFS/Houdini/viewer_states/examples/state_gadget_demo.hipを参照してください。

def onMouseEvent(self, kwargs):

    # カーソルのラベルを設定します。
    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)

        if gadget_name == "face_gadget":

            reason = ui_event.reason()          
            if reason == hou.uiEventReason.Start:

                # ロケートしたポリゴン法線に沿ってジオメトリを移動できるようにDraggerをセットアップします。
                self.scene_viewer.beginStateUndo("Drag")

                # ラインの開始位置と方向を取得します。
                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)                    

                # ガイドラインを配置します。
                self.guide_line_points[0].setPosition(line_orig)
                self.guide_line_points[1].setPosition(line_orig + line_dir*0.3)

                # ... あとは矢印
                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]:
                # 最新のDragger値を取得します。
                drag_values = self.dragger.drag(ui_event)
                delta = drag_values["delta_position"]

                # 移動パラメータを更新します。
                self.tx.set(self.tx.eval() + delta[0])
                self.ty.set(self.ty.eval() + delta[1])
                self.tz.set(self.tz.eval() + delta[2])

                # ガイドジオメトリを更新します。
                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:
                    # ドラッグを完了します。
                    self.dragger.endDrag()
                    self.guide_line.show(False)
                    self.guide_arrow.show(False)

            if reason != hou.uiEventReason.Located:
                # ガイドジオメトリのデータ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

ガイドジオメトリを生成する方法

  • 単純なガイドであれば、SOP Verbを空っぽのhou.Geometryオブジェクトに適用することで、プログラム的にガイドジオメトリを構築することができます。

    (このSOP Verbは、execute()に渡されるジオメトリオブジェクトを 上書き していることに注意してください。複数のジェネレータを構築するには、“バッファ”ジオメトリ内でexecute()を実行して、その結果を“メインの”ジオメトリにマージさせる必要があります。)

    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)
    

    Python SOPを使用することで、ジオメトリ生成スクリプトをプレビューしたり、テストしたり、デバッグすることができます。 このSOPは、ノードの Python Code パラメータによって構築されたhou.Geometryオブジェクトを出力します。

  • もっと複雑なガイドの場合だと、ステートがアセットに関連付けられていれば、アセット内の任意のSOPノードをクックすることで、ガイドジオメトリを生成することができます。

    これは、通常ではアセットの中で“ガイドジオメトリ”ネットワークをセットアップしてそのアセットのパラメータ値からガイドを構築させる時に強力です。

    ステートを実装したクラスをインスタンス化した時、そのインスタンスにはまだノードの参照を持っていないので、そのインスタンスの中でノードを参照することができません。 代わりに、onEnter()メソッド内でDrawableオブジェクトを作成してください。

    class MyState(object):
        def __init__(self, state_name, scene_viewer):
            self.state_name = state_name
            self.scene_viewer = scene_viewer
    
            # ここでは、ガイドジオメトリ用のDrawableオブジェクトを作成することができません。
            # その理由は、まだノードの参照がないからです。
            self._guide = None
    
        def onEnter(self, kwargs):
            # このメソッドは、このステートを使ってノードの参照を取得します。
            node = kwargs["node"]
            # これが私達のアセットであることを前提に、その中のいくつかのSOPsをクックします。
            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)
    
  • もちろん、 任意の ジオメトリノードをクックしてガイドジオメトリを生成することができます。

    例えば、あるジオメトリを他のジオメトリに整列させるツールを想像してみてください。 整列させるプリミティブを選択するスクリプトを組んでから、そのステートに入ったとします。そのステートは、選択したプリミティブを含んだガイドジオメトリオブジェクトを生成することができます。 ユーザが整列先のプリミティブ上にマウスを置いた時、その位置でガイドジオメトリをマウスポインタ下のプリミティブに整列させてワイヤーフレームで描画することで、その整列のプレビューを表示することができます。

Tip

Control SOPは、ジャッキや十字線などのカーソル、ガイド、マーカーを生成するのに便利なノードです。

Note

ガイドジオメトリは閉じたポリゴンメッシュだけで構成してください。 他のタイプのジオメトリは正しくレンダリングされない可能性があります。

ガイドを移動、回転、スケールさせる

  • hou.Matrix4オブジェクトを使ってDrawableオブジェクトのトランスフォームを設定すると、次回のビューアの再描画でその位置、向き、スケールが更新されます。

    別々のDrawableオブジェクトを別々にトランスフォームさせたいのであれば、各ガイドジオメトリを維持しなければならないことを忘れないでください。

  • 残念ながら、線形代数の話はこのドキュメントの範囲外です。hou.hmathモジュールには、行列を構築するための便利な関数が含まれており、hou.Matrix4オブジェクト自体に便利なメソッドが含まれています。これらの関数を学習してください。

    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)
    

便利なオブジェクトと関数

線形代数 と変換行列を説明するのは、このドキュメントの範囲を超えています。 しかし、行列を扱わなければならない時に役に立つ色々なオブジェクトと関数があることに気づくべきです。

Note

Houdiniは 行優先 の行列を使用します。これは、変換行列を解説しているチュートリアルやテキストブックによっては異なります。

  • hou.Matrix4オブジェクトは4×4行列を表現します。このオブジェクトにはinverted()transposed()といったユーティリティメソッドがあります。

  • hou.Vector3オブジェクトは位置(移動)、方向ベクトル、法線、スケール、オイラー回転角を表現するのに使用します。このオブジェクトにはdot()cross()といったユーティリティメソッドがあります。

    特定の用途に関連したユーティリティメソッドがいくつかあります。 例えば位置情報を持っていれば、distanceTo()またはangleTo()を使用することで、他の位置までの距離や角度を取得することができます。 方向ベクトルを持っていれば、length()またはlengthSquared()を使用することができます。

  • hou.hmath.identityTransformは、4×4の単位行列オブジェクトを作成します。

  • hou.Matrix4.explodeは、トランスフォーム行列から色々な“部分”を抽出します。これは、“移動”部分をVector3ポジションに、“回転”部分をオイラー回転角(度)を含んだVector3、“スケール”部分をスケールを含んだVector3にマッピングした辞書を返します。

  • hou.hmath.buildTransformは、hou.Matrix4.explodeで作成した辞書からMatrix4のトランスフォーム行列を作成します。ここに何かのキー(例えば、transform, rotate, shear)を含めることで、その行列を構築することができます。

  • hou.hmath.buildTranslate, hou.hmath.buildRotateAboutAxis, hou.hmath.buildRotate, hou.hmath.buildScaleはそれぞれ移動、回転、スケールの“部分”のみを含んだMatrix4トランスフォーム行列を構築します。これらの部分を組み合わせることができます。

  • hou.Matrix3.extractRotatesは、3×3の回転行列からオイラー角(度)を抽出します。hou.Matrix4.extractRotationMatrix3は、4×4トランスフォーム行列から3×3回転行列を抽出します。

ガイドトランスフォームを補正する方法

Drawableオブジェクトは常にワールド空間で自身のトランスフォームを解釈しますが、SOPステートで取得した光線はローカル空間です。 親のジオメトリオブジェクトのトランスフォームがデフォルトのままであれば、何も違いはありません。 しかし、SOPトランスフォームまたはポインティング光線(これはローカル空間です)に基づいてガイドジオメトリを表示させるために、その親ジオメトリオブジェクトをトランスフォームさせた場合、そのガイドジオメトリが間違った場所に表示されてしまいます。

ガイドジオメトリをワールド空間に適切に配置するには、その親のジオメトリオブジェクトを検索し、Drawableオブジェクトのトランスフォームを設定する前に、そのトランスフォームを適用してください。

ローカルの位置/回転ベクトルをワールド空間に変換する一般的な方法は以下のとおりです:

# ローカルの位置とローカルの回転があったと仮定します。
local_position = ...  # type: hou.Vector3()
local_rotate = ...  # type: hou.Vector3()

# ジオメトリオブジェクトのトランスフォームを補正します。
parent = ancestorObject(kwargs["node"])
if parent:
    parent_xform = node.parent().worldTransform()
    world_pos = local_pos * parent_xform
    world_rotate = local_rotate * parent_xform.extractRotates()
    # これは、以下のように記述することもできます。
    # world_rotate = local_rotate.multiplyAsDir(parent_xform)
else:
    world_pos = local_pos
    world_rotate = local_rotate

例として、以下は、マウスポインタの下に球の“カーソル”ガイドを表示する単純なステートです。これは親トランスフォームを取得し、それをカーソルに適用しています:

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):
        # 光線の原点と方向を取得する。
        ui_event = kwargs["ui_event"]
        ray_origin, ray_dir = ui_event.ray()

        # ローカル空間で交差を求めます!
        intersected = -1
        if node.inputs() and node.inputs()[0]:
            # 入力ジオメトリを取得します。
            geometry = node.inputs()[0].geometry()
            intersected, pos, _, _ = sopGeometryIntersection(geometry, origin, direction)
        if intersected < 0:
            # 入力ジオメトリもなければ、光線も当たらなかった場合は、
            # Construction Planeとの交差を試します。
            position = cplaneIntersection(self.scene_viewer, origin, direction)

        # ジオメトリコンテナを検索し... この場合だとnode.parent()で見つかることが多いですが、そのノードが1つ以上のサブネットの中にある場合の制御が必要です。
        parent = ancestorObject(kwargs["node"])
        # 見つかったコンテナのトランスフォームを使って、カーソルをワールド空間で表示します。
        parent_xform = parent.worldTransform()
        world_pos = position * parent_xform
        # ワールド空間の移動からMatrix4を構築します。
        m = hou.hmath.buildTranslate(world_pos)
        self._cursor.setTransform(m)
        self._cursor.show(True)

    def onInterrupt(self, kwargs):
        # ツールを一時停止した時にカーソルガイドを表示しないようにします。
        self._cursor.show(False)

ユーティリティ関数

親オブジェクトノードを検索する方法

SOPステートでは、場合によっては、SOPノードを含んだオブジェクトノードのメソッドまたはパラメータにアクセスしたいことがあります (例えば、オブジェクトレベルのトランスフォームを補正する場合です。親のオブジェクトノードはnode.parent()で取得できることが多いですが、ノードが1つ以上のサブネット内にある場合の制御が必要です。)

以下の関数にSOPノードを渡すと、その直近の親オブジェクトが返されます。

# 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

Pythonスクリプト

はじめよう

次のステップ

リファレンス

  • hou

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

導師レベル

Python Viewerステート

Pythonビューアハンドル

プラグインタイプ