環境: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 = (viewport.mapToScreen(c1) - viewport.mapToScreen(c0)).length()
if dist < self.edge_range and dist < closeDist:
closeDist = 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))