Houdini 17.0 Pythonスクリプト

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

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

On this page

Pythonビューアステート

概要

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

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

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

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

hou.Drawable APIは、ガイドジオメトリを表示させることができます。 このジオメトリソースには、hou.Geometryオブジェクトまたはhou.drawablePrimitive値(現在のところ、hou.drawablePrimitive.Sphereにのみ対応しています)を指定することができます。

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

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

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

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

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

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

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

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

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

    scene_viewer.curViewport().draw()
    

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

  • 単純なガイドであれば、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")
    
            self._guide = hou.Drawable(
                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は、ジャッキや十字線などのカーソル、ガイド、マーカーを生成するのに便利なノードです。

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

  • 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


class CursorState(object):
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

        self._cursor = hou.Drawable(
            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

Pythonビューアステート

See also

Pythonスクリプト

はじめよう

次のステップ

Pythonビューアステート

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

導師レベル

リファレンス

  • hou

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

  • Alembic拡張関数

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