ポリラインツール

環境:Houdini 20.0.751

Python Viewer Stateでビュー上にポイントを打ってポリラインを編集するツール。ポイントの新規追加、移動、削除、挿入と一通りのことができるようになっています。

ノードを選択した状態でエンターキーを押すことでViewer Stateのモードに入る。

概要

ポイントの新規作成:左マウスボタンでクリック
ポイントの移動:左マウスボタンでドラッグ
ポイントの削除:中マウスボタンでクリック
ポイントの挿入:エッジ上をクリックする

input0にコリジョンメッシュをつなぐと、メッシュに沿った座標で一連の処理が行われる。

内部のノード構成

Add SOPのみ。Add SOPを選択したままHDA化する。

Add SOPのNumber of Pointsをドラッグして

HDAのType PropertyのParametersにドロップしてリンクする。ここをコードで参照していく。

HDAのState Scriptのコード

HDAのType Propertyの、Interactive > State Script > New からSamplesのAdd Pointを選んで出てくるサンプルコードに加筆する要領で書いたコード。

import hou
import viewerstate.utils as su

class State(object):
    MSG = "Left Mouse Button to add points. Middle Mouse Button to delete points."

    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer
        
        self.pressed = False
        self.index = 0    # 新しいポイントのインデックス
        self.node = None
        
        self.drag = False   # ドラッグ中のフラグ
        self.dragIndex = 0  # ドラッグ中のポイントインデックス
        
        self.colli_geometry = None   # コリジョン用ジオメトリ
        self.guide_geometry = None   # ガイド用ポイントジオメトリ
        
        # ポイントとエッジの判定距離(ピクセル)
        self.point_range = 16
        self.edge_range = 8
        
        # UIのポイント(2D)
        point = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Point, "point",
            params = {
                "num_rings": 2,
                "radius": 4,
                "color1": (1.0,1.0,1.0,1.0),
                "style" : hou.drawableGeometryPointStyle.SmoothSquare}
        )
        
        self.poly_guide = hou.GeometryDrawableGroup("poly_guide")
        self.poly_guide.addDrawable( point )

    # Add SOPからポイント数を取得する
    def pointCount(self):
        try:
            multiparm = self.node.parm("points")
            return multiparm.evalAsInt()
        except:
            return 0

    # マウス左ボタンが押された時に呼ばれる処理
    def start(self):
        if not self.pressed:
            self.scene_viewer.beginStateUndo("Add point")
        self.pressed = True

    # マウス左ボタンが放された時に呼ばれる処理
    def finish(self):
        # 前フレームで押下されていたら
        if self.pressed:
            self.scene_viewer.endStateUndo()
        self.pressed = False

    # ガイドの表示・非表示
    def show(self, visible):
        self.poly_guide.show(visible)
        
    # ユーザーがStateに入ると呼び出される(エンターキーを押すと入る)
    def onEnter(self, kwargs):
        self.node = kwargs["node"]

        if not self.node:
            raise

        self.scene_viewer.setPromptMessage( State.MSG )
        
        # コリジョンのジオメトリ(input[0]にジオメトリが入力されている場合はジオメトリを設定する)
        inputs = self.node.inputs()
        if inputs and inputs[0]:
            self.colli_geometry = inputs[0].geometry()
          
        # ガイドのジオメトリ
        hda_node = self.scene_viewer.currentNode()
        if hda_node:
            self.guide_geometry = hda_node.node('add1').geometry()
        
        self.show(True)
        
    # Stateが中断された時に呼び出される
    def onInterrupt(self,kwargs):
        self.finish()
        self.show(False)

    # Stateの中断が再開された時に呼び出される
    def onResume(self, kwargs):
        self.scene_viewer.setPromptMessage( State.MSG )
        self.show(True)

    # マウスの移動やクリック時に呼び出される
    def onMouseEvent(self, kwargs):


        ui_event = kwargs["ui_event"]
        device = ui_event.device()
        origin, direction = ui_event.ray()

        # hou.GeometryViewportクラス
        viewport = self.scene_viewer.curViewport()
        
        # コリジョンジオメトリとの交差判定をする
        inputs = self.node.inputs()
        if inputs and inputs[0]:
            self.colli_geometry = inputs[0].geometry()
        else:
            self.colli_geometry = None
            
        if self.colli_geometry:
            hit, position, norm, uvw = su.sopGeometryIntersection(self.colli_geometry, origin, direction)
            # どことも交差がなければコンストラクション平面に交差させる
            if hit == -1:
                position = su.cplaneIntersection(self.scene_viewer, origin, direction)
        else:
                position = su.cplaneIntersection(self.scene_viewer, origin, direction)
            
        # 左ボタンの押下
        if device.isLeftButton():
            # ポイントをドラッグ中
            if self.drag == True:
                self.node.parmTuple("pt%d" % self.dragIndex).set(position) # 座標をセット
                
            else:
                # ポイントの移動
                # マウスがクリックしたポイントの番号を距離から探す
                closeIndex = -1
                closeDist = 1000000
                for i in range(self.pointCount()):
                    A = origin
                    parm_pos = self.node.parmTuple("pt%d" % i).eval() # parmから値を取得するにはeval()を使う
                    P = hou.Vector3((parm_pos[0],parm_pos[1],parm_pos[2]))
                    dist = (viewport.mapToScreen(P) - viewport.mapToScreen(A)).length()
                        
                    if dist < self.point_range and dist < closeDist:
                        closeDist = dist
                        closeIndex = i
                
                # ポイントをクリックしていたらドラッグ(移動)フラグを立てる
                if closeIndex != -1:
                    self.dragIndex = closeIndex
                    self.drag = True
                    self.start()
                
                # エッジの近くをクリックしたら中間点を挿入する
                else:

                    # エッジごとの判定処理
                    # マウスポインタからの線分と各エッジを最短で結ぶ直線と交点を計算している
                    closeIndex = -1
                    closeDist = 1000000
                    closePos = hou.Vector3()
                    for i in range(self.pointCount()-1):
                        A = origin
                        B = origin + direction
                        parm_pos = self.node.parmTuple("pt%d" % i).eval()
                        C = hou.Vector3((parm_pos[0],parm_pos[1],parm_pos[2]))
                        parm_pos = self.node.parmTuple("pt%d" % int(i+1)).eval()
                        D = hou.Vector3((parm_pos[0],parm_pos[1],parm_pos[2]))
                        
                        ab = (B-A).normalized()
                        cd = (D-C).normalized()
                        ac = (C-A)
                        
                        d0 = (ab.dot(ac)-cd.dot(ac)*ab.dot(cd))/(1-(ab.dot(cd)*ab.dot(cd)))
                        d1 = (ab.dot(ac)*ab.dot(cd)-cd.dot(ac))/(1-(ab.dot(cd)*ab.dot(cd)))
                        
                        # 線分の範囲内か判別
                        if d1 >= 0 and d1 < (D-C).length():
                            c0 = A + ab * d0
                            c1 = C + cd * d1
                            dist = (c1 - c0).length()

                            view_space_pos0 = viewport.mapToScreen(c0)
                            view_space_pos1 = viewport.mapToScreen(c1)
                            view_space_dist = (view_space_pos1 - view_space_pos0).length()
                            
                            if view_space_dist < self.edge_range and view_space_dist < closeDist:
                                closeDist = view_space_dist
                                closeIndex = i
                                closePos = c1

                    # エッジの近くがクリックされていたら、エッジの中間点にポイントを挿入する
                    if closeIndex != -1:
                        self.start()
                        closeIndex += 1
                        multiparm = self.node.parm("points")
                        multiparm.insertMultiParmInstance(closeIndex)
                        self.node.parmTuple("pt%d" % closeIndex).set(closePos) # 座標をセット

                    # どこにもあてはまらなければ、新しいポイントを生成する
                    else:
                        self.start()
                        self.index = self.pointCount()
                        multiparm = self.node.parm("points")
                        multiparm.insertMultiParmInstance(self.index)
                        self.node.parm("usept%d" % self.index).set(1) # 表示をオン
                        self.node.parmTuple("pt%d" % self.index).set(position) # 座標をセット
            
        else:
            self.finish()
            self.drag = False
            
        # 中ボタンを押した時の処理
        if device.isMiddleButton():
            
            # マウスがクリックしたポイントの番号を距離から探す
            closeIndex = -1
            closeDist = 1000000
            for i in range(self.pointCount()):
                A = origin
                parm_pos = self.node.parmTuple("pt%d" % i).eval() # parmから値を取得するにはeval()を使う
                P = hou.Vector3((parm_pos[0],parm_pos[1],parm_pos[2]))

                dist = (viewport.mapToScreen(P) - viewport.mapToScreen(A)).length()
                    
                if dist < self.point_range and dist < closeDist:
                    closeDist = dist
                    closeIndex = i
                    
            # ポイントを削除する
            if closeIndex != -1:
                multiparm = self.node.parm("points")
                multiparm.removeMultiParmInstance(closeIndex)
         
        # UIのジオメトリを描画する
        if self.pointCount() > 0:
            poly_points = self.guide_geometry.prim(0).points()
            poly_geo = hou.Geometry()
            poly = poly_geo.createPolygon()
            for pt in poly_points:
                point = poly_geo.createPoint()
                point.setPosition(pt.position())
                poly.addVertex(point)
                
            # update the drawable
            self.poly_guide.setGeometry(poly_geo)
            self.show(True)
                
        return True

    def onDraw( self, kwargs ):
        """ This callback is used for rendering the drawables
        """
        handle = kwargs["draw_handle"]
        self.poly_guide.draw(handle) 

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 = "ViewerStateAddPoint::1.0"
    state_cat = hou.sopNodeTypeCategory()

    template = hou.ViewerStateTemplate(state_typename, state_label, state_cat)
    template.bindFactory(State)
    template.bindIcon(kwargs["type"].icon())

    return template

ジオメトリとの交差判定

交差判定にはsopGeometryIntersection()を使う。

スクリーンスペース上のマウスポインタ座標をワールド空間の始点と方向ベクトルに変換する場合は以下のように記述する。originに始点、directionにベクトルが格納される。

ui_event = kwargs["ui_event"]
origin, direction = ui_event.ray()

相対パスの取り方

HDA内の特定のSOPを相対パスで参照する場合

# HDA内のSOPジオメトリを取得する
hda_node = self.scene_viewer.currentNode()
colli_geometry = hda_node.node('Colli').geometry()

スクリーン空間とワールド空間の判定処理

マウスポインタとポリラインのエッジの判定はこの計算を使っている。
直線同士を結ぶ直線を求める

UIのポイント

UIのポイントはhou.GeometryDrawableGroupを使っている。カメラのズームに依存することなくスクリーン上で同じ大きさで描画される。GeometryDrawableGroupはGeometryDrawable(PointやLine、Face)を子として格納している。
https://www.sidefx.com/ja/docs/houdini/hom/hou/GeometryDrawable.html
https://www.sidefx.com/ja/docs/houdini/hom/hou/GeometryDrawableGroup.html

Undo処理

scene_viewer.beginStateUndo()で始め、scene_viewer.endStateUndo()で終えるとその区間がひとブロックのアンドゥとなる。

デバッグ

self.log()を使う。

Viewer State Browserに結果が表示される。

self.log(position)

オブジェクトのプロパティや関数を調べる場合はdir()を使う。

self.log(dir(device))
タイトルとURLをコピーしました