Houdiniプラグイン開発備忘録4 ~ カスタムSOPにカスタムパラメータを追加する編

今回は、カスタムノードにカスタムパラメータを追加してみます


VerbとProtoヘッダファイル

 最近のHDKでは、SOPノード作成にVerbという仕組みを使うことが多いようです。
 Verbというのは、かなりざっくりいうと、SOPの振る舞いを定義する際、ジオメトリへの処理と、それ以外のパラメータ操作などの処理を分離する仕組みです。

[参考]プログラム的にVerb(動詞)を使ったジオメトリ

 Verbを利用してSOPを作成する際、これまで何度か言及していた、「Protoヘッダファイル」をビルド手順の中で生成し利用することになります。


Protoヘッダファイルの主な役割

 Protoヘッダファイルには、ジオメトリ処理以外の実装が自動的に記述されます。

※これは現時点での理解なので間違っているかもしれませんが、この仕組みにより、開発者は面倒なパラメータとジオメトリ処理をつなぐ実装を省略し、主にジオメトリ操作に関する処理に注力すれば良くなります。

 主なProtoヘッダファイルの役割は以下のようなものになります。

  1. PRM_Templateを生成する
  2. パラメータの操作を簡単化する関数群を自動生成する

Protoヘッダファイルの作り方

 Protoヘッダファイルは何もしなくても勝手に作成されるわけではなく、ソースコードの中にそのノードがどのようなパラメータを持っているかを示す「Dialog Script」を記述、または「*.ds形式のファイル」として用意して、PRM_TemplateBuilderに与える必要があります。

 このDialog Scriptは、ソースコード中で *theDsFile に記述します。

static const char *theDsFile = R"THEDSFILE(
・・・
)THEDSFILE";
//SOP_Starの例

static const char *theDsFile = R"THEDSFILE(
{
    name        parameters
    parm {
        name    "divs"      // Internal parameter name
        label   "Divisions" // Descriptive parameter name for user interface
        type    integer
        default { "5" }     // Default for this parameter on new nodes
        range   { 2! 50 }   // The value is prevented from going below 2 at all.
                            // The UI slider goes up to 50, but the value can go higher.
        export  all         // This makes the parameter show up in the toolbox
                            // above the viewport when it's in the node's state.
    }
    parm {
        name    "rad"
        label   "Radius"
        type    vector2
        size    2           // 2 components in a vector2
        default { "1" "0.3" } // Outside and inside radius defaults
    }
    parm {
        name    "nradius"
        label   "Allow Negative Radius"
        type    toggle
        default { "0" }
    }
    parm {
        name    "t"
        label   "Center"
        type    vector
        size    3           // 3 components in a vector
        default { "0" "0" "0" }
    }
    parm {
        name    "orient"
        label   "Orientation"
        type    ordinal
        default { "0" }     // Default to first entry in menu, "xy"
        menu    {
            "xy"    "XY Plane"
            "yz"    "YZ Plane"
            "zx"    "ZX Plane"
        }
    }
}
)THEDSFILE";

Dialog Scriptを生成する

 頑張って調べてみましたが、この記事を執筆している時点では、Dialog Scriptに関するリファレンスマニュアルのようなものを見つけることは出来ませんでした。
 そのかわり、リファレンスに頼ってフルスクラッチで書かなくとも、簡単にDialog Scriptを生成する方法があったので紹介します。

 やり方は、概ね以下の手順になります。

  1. パラメータ定義を抽出するためHDAを作成
  2. HDAに、カスタムノードに持たせたいパラメータを作成
  3. HDAのhou.ParmTemplateGroup.asDialogScript()でDialog Scriptのコードを取得
# Dialog Scriptを出力するサンプル
hda_node = hou.node("/to/hda/node/path")
hda_type = hda_node.type()
hda_definition = hda_type.definition()
hda_parm_template_group = hda_definition.parmTemplateGroup()
hda_dialog_script = hda_parm_template_group.asDialogScript()
print(hda_dialog_script)

 これにより、そのHDAに定義されたパラメータ定義が、Dialog Scriptの形式で取得できます。

 その後、ソースコードの*theDsFileに、取得したコードをそのままコピペするなどして与えた後、いつも通りの手順でビルドするだけです。


テスト

 以下は、HDAとして標準で用意されているColor SOPからDialog Scriptを頂いて、カスタムSOPにそのまま持たせてみたものです。


以上、カスタムSOPにカスタムパラメータを追加する方法でした。
なにか間違いや、不明な点などあれば、コメントいただけると嬉しいです。

Houdini – Python(HOM)によるノード選択(改題)

 あるシーンを開いた時、膨大なオブジェクト数とグループ数により階層が深く、なおかつすべてのノードに命名規則が全く見当たらないという、ツール作成者視点でシーンを眺めた時、ついつい汚い床の上をのたうち回りたくなるような状況は往々にしてあることなのですが、そのような状況下でのHoudiniの選択操作は、他のDCCツールよりも遅れを取っている気がします。

 例えば、ビューポートで選択しているすべてのノードの親を簡単にまとめて再選択したかったり、同様に、選択アイテムのすべての下流ノードをまとめて選択したかったりと言った状況です。

 ノードを右クリックして表示されるポップアップメニューから、Inputs/Outputsサブメニューを駆使してみるも、単一の選択アイテムのみがノード検索の起点になるので、複数のノードツリーをまとめて選択できなかったりします。

 このように、意外とHoudiniデフォルトの選択機能に痒いところが多かったので、選択補助スクリプトを書いてみました。

 ついでと言ってしまうと何だか申し訳なく感じてしまうのですが、簡単なメモを書き残したいと思います。


基本的なノード検索

import hou
nodeObject = hou.node("node_path")
import hou
selectedItems = hou.selectedItems()

ノードの親子関係

import hou
node = hou.node('node_path')

# parent()は自身を含む外側のノードの取得
outerNode = node.parent()
print( outerNode )

# children()は自身が含む内側のノードリストを取得
innerNodes = node.children()
print( innerNodes )
import hou
node = hou.node('node_path')

# inputs()は自身へ入力している直接の上流ノードリストの取得
inputNodes = node.inputs()
print( inputNodes )

# outputs()は自身が直接出力しているノードリストを取得
outputNodes = node.outputs()
print( outputNodes )

# inputAncestors()は直系の祖先をまとめて取得
ansectorNodes = node.inputAncestors()
print( ansectorNodes )

かなり基本的なところではこんな感じ。
他にもたくさんあるので、その他のメソッドは要マニュアル参照。

出力の全子孫を取得するメソッドがパット見見つからない感じだったので、再帰的に子孫をたどる関数を作ってみる。

import hou

def getDescendents(node, *args, **kwargs):
    """
    DESCRIPTION : 引数で与えたノードから再帰的に子孫を辿って返す
      ARGUMENTS : node : 探索を開始するノード
         RETURN : descendents : 見つかった子孫ノードリスト
    """
    descendents = []

    direct_outputs = node.outputs()
    if direct_outputs:
        for direct_output in direct_outputs:
            descendents += [direct_output]
            descendents += getDescendents(direct_output)
   
    return descendents
    
node = hou.node('node_path')
descendents = getDescendents(node)
for descendent in descendents:
    print( descendent )

選択操作

import hou
node = hou.node("node_path")

# 選択 setSelectedメソッドにブール値をセット 1で選択0で非選択
node.setSelected(1)

# 選択解除
node.setSelected(0)

 上記を組み合わせて、現在のノードから1つ上流のノードを探し、再度全下流ノードを探して全選択を行うなどの便利なコマンドを作成できます。

MYAM_QuickSelector 途中経過

今年の1月末くらいに作り始めたのはいいものの、公私ともに忙しく、なかなか手がつけられなかったツール「MYAM_QuickSelector」が、このGWでだいぶ形になってきたので、途中経過を駆け足で記事にしてみます。

Qtは目的に到達するまでの手続きが多く面倒な部分も多いですが、慣れるて来るととても楽しいです。もうMaya標準のGUIライブラリは触れません。


■画面サンプル

bandicam 2016-05-08 22-32-34-391

好きな画像をツール上からキャプチャし、背景に置いた状態で好きなノードに対応するボタンを配置し、いつでも簡単に選択できるようになります。
選択できるアイテムはいわゆるDAGオブジェクトだけでなく、マテリアルなどのDGノードにも対応します。


■マルチプラットフォーム(DCCツール間)

bandicam 2016-05-08 23-10-05-047

Maya上でもHoudini上でも全く同様に動作します。(現在はMayaのみ対応ですが)
その他にも、最小限の拡張でPySideが使えるすべてのDCCツールに対応可能です。
DCCツールを変えた時にも、操作法が統一されていると覚え直す必要もなく楽です。


■キャプチャ機能

bandicam 2016-05-08 23-13-10-644

Bandicamのようにウインドウを使ってキャプチャ範囲を指定して、背景画像として取り込むことができます。


■セレクターボタンは任意の画像が使えます

bandicam 2016-05-08 23-12-43-450

ジョイントやコントロールリグなど、タイプごとに任意の画像をアイコンとして設定できます。
また、各アイコンのサイズも自由に変更できるため、よく使うコントローラを目立たせておくなど、視認性を高められます。
アイコン置き場フォルダに任意の画像を格納しておけば、自動的にツール内で使用できるようになります。


■その他

その他にも、配置したボタンの整列なども手軽に行なえます。


もう少し洗練して、早くリリースしたいなあ。

MYAM_genericSelector(仮名)を作り始めました

昔とある仕事場で作成した MYAM_quickSelector2 の後継ツールをQtで作り始めました。

前述のツールの機能に加え、足したい機能は大体以下の様な感じです

・オブジェクトアイコンに任意の画像が使用できる
・オブジェクトアイコンのサイズや形状を自由に変更できる
・Maya/MotionBuilder/Houdiniあたりで共通して使えるようにする
・選択オブジェクトのグループ登録/簡単選択

まだほとんど仮組みの状態ですが、完成したらCreative Crashあたりで公開すると思います。

MYAM_genericSelector_SS001

過去に作成したツールのご紹介

リンク

テクニカルアーティストを自称しているのに、あまりにも普段からツールネタが少ないのはどうかと思っていたところ、許可がいただけたので、自分がこれまで作ってきたツールの一部を載せてみたいと思います。

ご興味のある方はどうぞご覧ください。

 

Maya – リソースイメージを抽出

import maya.cmds as cmds
resources = cmds.resourceManager()
for resource in resources:
  saveDirPath  = "D:/MYAM_temp"
  saveFilePath = "%s/%s" % ( saveDirPath , resource )
  cmds.resourceManager( saveAs=(resource , saveFilePath ) )

ツールのGUIを作ってる時、Mayaのリソースイメージをそのまま流用したいことがある。
わざわざキャプチャしたりするのは面倒なので、上記のコマンドでごっそりイメージファイルを抜き取って、必要なアイコンを使ってしまおう。


こんな感じで簡単にアイコンを抽出出来た。

Python SOPでデフォーマーを作ってみる

リクエストがあったのでVOP SOPのような処理をPython SOPで行う方法を書いてみる。
とりあえず、ノード構成はこんな感じでやってみる。

■Python SOP

myDeformerはPython SOP
Python SOPはデフォルトの状態だと以下のようにコードが記述されている。

###########################################
node = hou.pwd()
geo = node.geometry()

# Add code to modify contents of geo.
# Use drop down menu to select examples.
###########################################

 

hou.pwd()はPython SOP自身のノードオブジェクトを返す。
Node.geometry()は自身に入力されたhou.Geometry型のジオメトリオブジェクトを返す。

geometryの中にはPointやEdgeやPrimitive型のオブジェクトが含まれており、実際に形状や色を変更する際は、これらのコンポーネントオブジェクトを取得して操作する。

■コンポーネントの取得

■Pointリストの取得
points = geo.points()

■Primitiveリストの取得
prims = geo.prims()

それぞれのコンポーネントに一律の処理を行うには?

 

・単純にforループで回す
for point in points:
  position = point.position()

for prim in prims:
  verts = prim.vertices()

・iterPointsを使う(generatorの使用、こっちのほうがメモリ効率良さそう)
for point in geo.iterPoints():
  position = point.position()

 

■pointを移動してみるサンプル

###########################################
import math

node = hou.pwd()
geo = node.geometry()

# Add code to modify contents of geo.
# Use drop down menu to select examples.
for point in geo.points():
  initPosition = point.position()
  pointIndex = point.number()
  radian = math.radians( pointIndex )
  displacement = hou.Vector3( [ 0 , math.sin( radian * 10 ) , 0 ] )
  newPosition = initPosition + displacement
  point.setPosition( newPosition )
###########################################

 

 

・Gridを変形

・Sphereを変形

■各オブジェクトのリファレンスマニュアル

各オブジェクトの使い方は以下のマニュアルを参照すべし

・hou.Geometry
http://sidefx.jp/doc/hom/hou/Geometry.html

・hou.Point
http://sidefx.jp/doc/hom/hou/Point.html

・hou.Prim
http://sidefx.jp/doc/hom/hou/Prim.html

Maya Python から Maya APIに進む

スクリプトで出来ることは、ほとんど出来る気がしてきたので、次は、一度挫折したC++ APIに着手していこうと思う。
PythonでクラスとかのOOP慣れしたせいか、意外とサクサク勉強が進んでる。
この調子だと、半年かからずにそこそこのプラグインが量産できるようになりそうな予感。