Point Place Tool

公開 更新

環境:Houdini 20.0.751

ポイントを配置していくツール

Parameterの構成

Folder Type: Multiparm Block (tabs) Name: points Label: Points First Instanceを0に設定する

Name: position_# Label: Position Type: Float Vector 3

Name: id_# Label: ID Type: String

Viewer Stateのコード

#
# Point Placement Tool 20250321
# コリジョン上にポイントを配置するツール
#
import hou
import viewerstate.utils as su
from enum import Enum
import uuid
import math

class State(object):

    MSG = "Select the polyline and press Ctrl to edit the points."

    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer
        
        # UIのテンプレート
        HUD_TEMPLATE = {
            "title": "Point Place", "desc": "tool", "icon": "SOP_subnet",
            "rows": [
                {"id": "add_act", "label": "Add/Select Point", "key": "LMB"},
                {"id": "del_act", "label": "Delete Point", "key": "Del"},
                {"id": "focus_act", "label": "Camera Focus", "key": "f"},
            ]
        }
        
        # テンプレートを使用してHUDを設定します。通常では、これはステートが有効な間に一回実行します。
        self.scene_viewer.hudInfo(hud_template=HUD_TEMPLATE)

        self.viewport = None
        
        self.pressed = False    # マウス左ボタン押下フラグ
        self.node = None        # ノード
        
        self.drag = False   # ドラッグ中のフラグ
        self.dragIndex = 0  # ドラッグ中のポイントインデックス

        self.isPressedShift = False # SHIFTキーの押下判定フラグ
        
        self.selected_point = -1     # 選択中のプリミティブ番号
        self.intersect_point = -1    # 交差中のプリミティブ番号
        
        self.colli_geometry = None      # コリジョン用ジオメトリ
        self.select_geometry = None     # 結果のジオメトリ
        self.drawableGeo = None         # 入力されたポリラインのジオメトリ(表示用)
        
        # ポイントとエッジの判定距離(ピクセル)
        self.point_range = 16
        self.edge_range = 8

        # Construct a geometry drawable group
        # ポイント
        edit_point = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Point, "point",
            params = {
                "num_rings": 2,
                "radius": 3,
                "color1": (1.0,0.7,0.1,1.0),
                "style" : hou.drawableGeometryPointStyle.LinearSquare}
        )
        self.guideEditPoint = hou.GeometryDrawableGroup("poly_edit")
        self.guideEditPoint.addDrawable(edit_point)

        # ポイント(選択中)
        selected_point = hou.GeometryDrawable(self.scene_viewer, hou.drawableGeometryType.Point, "point",
            params = {
                "num_rings": 2,
                "radius": 5,
                "color1": (1.0,0.6,0.1,1.0),
                "style" : hou.drawableGeometryPointStyle.LinearSquare}
        )
        self.guideSelectedPoint = hou.GeometryDrawableGroup("poly_selected_point")
        self.guideSelectedPoint.addDrawable(selected_point)
            
    # UIのMultiparmフォルダからプリミティブ数を取得する
    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
        
    # ユーザーがStateに入ると呼び出される(エンターキーを押すと入る)
    def onEnter(self, kwargs):
        self.node = kwargs["node"]

        if not self.node:
            raise

        self.scene_viewer.setPromptMessage( State.MSG )
        
        # コリジョンのジオメトリ(input[1]にジオメトリが入力されている場合はジオメトリを設定する)
        inputs = self.node.inputs()
        if inputs and inputs[0]:
            self.colli_geometry = inputs[0].geometry()
        else:
            self.colli_geometry = None
          
        # ガイドのジオメトリ
        hda_node = self.scene_viewer.currentNode()
        hda_node.cook(force=True)
        if hda_node:
            self.select_geometry = hda_node.node('generate_points').geometry()

            if inputs and inputs[0]:
                #hda_node.node('geo_draw').cook(force=True) # 強制的にクック
                self.drawableGeo = hou.SimpleDrawable(self.scene_viewer, hda_node.node('geo_draw').geometry(), "drawable_geometry")
                self.drawableGeo.setDisplayMode(hou.drawableDisplayMode.WireframeMode)
                self.drawableGeo.enable(True)
                self.drawableGeo.show(True)

        self.guideEditPoint.show(True)
        self.guideSelectedPoint.show(True)
            
    # Stateが中断された時に呼び出される
    def onInterrupt(self,kwargs):
        self.finish()
        #self.drawableGeo.show(False)
        self.guideEditPoint.show(False)
        self.guideSelectedPoint.show(False)

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

    # 編集ポイントdrawbleの更新      
    def updateGuideEditPoint(self):
        # UIのジオメトリを描画する
        if self.pointCount() > 0:
            poly_geo = hou.Geometry()
            poly = poly_geo.createPolygon()

            # ジオメトリから座標を取得する
            poly_points = self.select_geometry.points()
            for i in range(len(poly_points)):
                parm_pos = poly_points[i].position()
                point = poly_geo.createPoint()
                point.setPosition(parm_pos)
                poly.addVertex(point)
                
            # update the drawable
            self.guideEditPoint.setGeometry(poly_geo)

    # 選択済みポイントdrawbleの更新  
    def updateGuideSelectedPoint(self):
        poly_geo = hou.Geometry()
        poly = poly_geo.createPolygon()
        
        if self.selected_point != -1:
            poly_points = self.select_geometry.points()
            parm_pos = poly_points[self.selected_point].position()
            point = poly_geo.createPoint()
            point.setPosition(parm_pos)
            poly.addVertex(point)

        self.guideSelectedPoint.setGeometry(poly_geo)
        
    #
    # マウスポインタとポイントの交差判定(交差していたらインデックス番号を、していなかったら-1を返す)
    #
    def isIntersectPoints(self, origin, viewport, positions, radius):
        closeIndex = -1
        closeDist = 1000000
        for i in range(len(positions)):
            A = origin
            P = positions[i]
            dist = (viewport.mapToScreen(P) - viewport.mapToScreen(A)).lengthSquared()

            if dist < pow(radius, 2) and dist < closeDist:
                closeDist = dist
                closeIndex = i
                
        return closeIndex

    # IDからMultiparmのインデックス番号を返す
    def getMultiparmIndexFromId(self, id):
        index = -1
        for i in range(self.node.parm("points").evalAsInt()):
            uuid = self.node.parm("id_%d" % i).eval()
            #self.log(uuid)
            if id == uuid:
                index = i
                break

        return index

    # PrimitiveのインデックスからMultiparmのインデックス番号を返す
    def getMultiparmIndexFromPoint(self, point_index):
        id = self.select_geometry.point(point_index).attribValue("id")
        multiparm_index = self.getMultiparmIndexFromId(id)
        return multiparm_index

    #
    # マウスイベント
    #
    def onMouseEvent(self, kwargs):

        ui_event = kwargs["ui_event"]
        device = ui_event.device()
        origin, direction = ui_event.ray()
        self.viewport = self.scene_viewer.curViewport() # hou.GeometryViewportクラス

        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:
                # スナップをいれる
                closeIndex = -1
                snapFlag = False

                if self.isPressedShift == False:
                    self.node.parmTuple("position_%d" % self.selected_point).set(position) # 座標をセット
                else:
                    # 画面に対して平面方向にドラッグする
                    parm_pos = self.node.parmTuple("position_%d" % self.selected_point).eval() # parmから値を取得するにはeval()を使う
                    P = hou.Vector3(parm_pos[0],parm_pos[1],parm_pos[2])
                    planeNormal = direction

                    flagHorizontal = False
                    angle = math.acos(direction.dot(hou.Vector3(0, 1, 0))) 
                    if math.radians(45) < angle and angle < math.radians(135):
                        flagHorizontal = True

                    if snapFlag == True:
                        if flagHorizontal:
                            newPos = P.x(), list_pos[closeIndex][1], P.z() # Y座標だけ変更したい場合はこの変数を使う
                        else:
                            newPos = list_pos[closeIndex][0], P.y(), list_pos[closeIndex][2]
                    else:
                        d = -(planeNormal.x() * P.x() + planeNormal.y() * P.y() + planeNormal.z() * P.z())
                        t = -(planeNormal.x() * origin.x() + planeNormal.y() * origin.y() + planeNormal.z() * origin.z() + d) / (planeNormal.x() * direction.x() + planeNormal.y() * direction.y() + planeNormal.z() * direction.z());
                        cross = origin.x() + direction.x() * t, origin.y() + direction.y() * t, origin.z() + direction.z() * t  # Vector3ではなくタプル
                        
                        if flagHorizontal:
                            newPos = P.x(), cross[1], P.z() # Y座標だけ変更したい場合はこの変数を使う
                        else:
                            newPos = cross[0], P.y(), cross[2] # Y座標だけ変更したい場合はこの変数を使う

                    self.node.parmTuple("position_%d" % self.selected_point).set(newPos)
  
            # ドラッグしてない場合
            else:
                # ポイントの移動
                # マウスがクリックしたポイントの番号を距離から探す
                list_pos = []
                # ジオメトリから座標を取得する
                poly_points = self.select_geometry.points()
                for i in range(len(poly_points)):
                    parm_pos = poly_points[i].position()
                    list_pos.append(hou.Vector3((parm_pos[0],parm_pos[1],parm_pos[2])))

                closeIndex = self.isIntersectPoints(origin, self.viewport, list_pos, self.point_range)

                # ポイントをクリックしていたらドラッグ(移動)フラグを立てる
                if closeIndex != -1:
                    # 選択中のポイントだったら選択解除する
                    if self.selected_point == closeIndex:
                        self.selected_point = -1
                    else:
                        self.dragIndex = closeIndex
                        self.drag = True
                        self.selected_point = closeIndex
                        self.start()
                        
                        # タブを選択する
                        multiparm = self.node.parm("points")
                        multiparm_index = self.getMultiparmIndexFromPoint(self.selected_point)
                        hou.ui.setMultiParmTabInEditors(multiparm, multiparm_index)
                
                # ポイントを新規追加する
                else:
                    # 選択中のポイントがあれば選択解除する
                    if self.selected_point != -1:
                        self.selected_point = -1
                    else:
                        self.start()
                        pt_index = self.pointCount()
                        
                        multiparm = self.node.parm("points")
                        multiparm.insertMultiParmInstance(pt_index)
                        
                        # UUID
                        id = str(uuid.uuid4())[:8]
                        self.node.parm("id_%d" % pt_index).set(id) # uuidをセット

                        # Position
                        self.node.parmTuple("position_%d" % pt_index).set(position) # 座標をセット
                        self.log("create new point")
                        self.selected_point = -1

                        # タブを選択する
                        hou.ui.setMultiParmTabInEditors(multiparm, pt_index)
        else:
            self.finish()
            self.drag = False
                    
        self.updateGuideEditPoint()
        self.guideEditPoint.show(True)
        self.updateGuideSelectedPoint()
        self.guideSelectedPoint.show(True)

        return True

    # キーボード入力
    def onKeyEvent(self,kwargs):
        ui_event = kwargs["ui_event"]
        self.key_pressed = ui_event.device().keyString()

        # Delキーが押されたら削除する
        if self.key_pressed in ("Del"):

            if self.selected_point != -1:
                multiparm = self.node.parm("points")
                multiparm.removeMultiParmInstance(self.selected_point)
                self.selected_point = -1
                self.intersect_point = -1

            self.updateGuideEditPoint()
            self.guideEditPoint.show(True)
            self.updateGuideSelectedPoint()
            self.guideSelectedPoint.show(True)

            return True

        # カメラのピボットを選択しているポイント座標にする
        if self.key_pressed in ("f"):
            if self.selected_point != -1:

                parm_pos = self.node.parmTuple("position_%d" % (self.selected_point)).eval()
                ptPos = hou.Vector3((parm_pos[0],parm_pos[1],parm_pos[2]))

                viewTransform = self.viewport.viewTransform()
                tuples = viewTransform.asTuple()
                worldCamPos = hou.Vector3(tuples[12], tuples[13], tuples[14])
                
                # カメラのピボットを変更する
                cam = self.viewport.defaultCamera() # hou.GeometryViewportCamera
                camPivotPos = hou.Vector3(cam.pivot())
                dist = (worldCamPos - camPivotPos).length()

                cam.setPivot(ptPos)
                cam.setTranslation(ptPos + hou.Vector3(0, 0, dist))

            return True

        self.key_pressed = None
        return False

    # 修飾キーのイベント
    def onKeyTransitEvent(self, kwargs):
        ui_event = kwargs["ui_event"]
            
        # SHIFTキー
        if ui_event.device().isShiftKey() == 1:
            self.isPressedShift = True
        else:
            self.isPressedShift = False

        return False

    def onDraw( self, kwargs ):
        """ This callback is used for rendering the drawables """
        handle = kwargs["draw_handle"]
        self.guideEditPoint.draw(handle)
        self.guideSelectedPoint.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 = "ViewerStatePointPlaceTool::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

HDA内のWrangleコード

int numpoints = `chs("../points")`;

for(int i = 0; i < numpoints; i++)
{
    int point_count = chi("../points_" + itoa(i));
    
    vector p = chv(sprintf("../position_%g", i));
    int pt = addpoint(0, p);
}

← 記事一覧へ