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にカスタムパラメータを追加する方法でした。
なにか間違いや、不明な点などあれば、コメントいただけると嬉しいです。

Refactoring Houdini Node Network – メンテナンス性の高いノードネットワーク構築のために

■はじめに

 この記事は、「Houdini Advent Calendar 2018 – 8日目」の記事です。

 既存のノードネットワークを変更する際、変更する対象が巨大であるためにどこを変更すべきかわかりにくくなってしまっていたり、ある箇所への変更が他の箇所に影響してしまい、メンテナンスしづらい状況を経験したことは誰にでもあると思います。

 この記事では、そのような状況を改善・予防するため「リファクタリング」と呼ばれる手法に焦点を当て、筆者の主観をもとに基本的な手法を紹介したいと思います。

■対象読者

・Houdini初級~中級者
・メンテナンスしにくいノードネットワークに日々苦しんでいる方
・メンテナンスしやすい設計でノードネットワークを構築したい方
・プログラミング初心者で、読みやすいコードを書くための作法を知りたい方


■リファクタリングとはなにか?

 リファクタリング (refactoring) とは、コンピュータプログラミングにおいて、プログラムの外部から見た動作を変えずソースコードの内部構造を整理することである。また、いくつかのリファクタリング手法の総称としても使われる。ただし、十分に確立された技術とはいえず、また「リファクタリング」という言葉に厳密な定義があるわけではない。(Wikipediaより抜粋)

※ここでいう「プログラムの外部から見た動作を変えず」というのは、リファクタリング前後で、リファクタリングする対象が行う処理の結果を変えないという意味です。


■Houdiniでのリファクタリング

 Houdiniでの作業はプログラミング色が強く、ノードネットワークを組み立てることはプログラミングすることそのものと言えます。
 ノードネットワークは、ある種ソースコードのようなものです。
 更に、HoudiniではVEX/Wrangle/Pythonなど、プログラミングそのものを行う要素も含んでいます。

 そのため「リファクタリング」手法のいくつかをHoudiniでも比較的素直に取り入れることができます。


■リファクタリングのメリット

 リファクタリングにより、ノードネットワークの構造を整理することができ、結果として以下のようなメリットが得られます。

・ノードネットワークを局所的/全体的に理解しやすくなる
・ノードネットワークの修正(保守)が簡単になる
・ノードネットワークの拡張が簡単になる
・理解しやすく整理された形で新たなノードネットワークを作れるようになる


■手法ごとの具体的な方法

□名前の変更

 ノード名、パラメータ名、アトリビュート名、Wrangleの変数名など、各種「名前付きの要素」を、誰が見てもわかりやすい形にリネームします。

 これにより、その要素の役割が判断しやすくなり、時間が経ってから見直したときや、他人へデータを渡したときに処理の内容を理解する助けになります。

・名前のプリフィクス(接頭辞)について

set_, del_ などの部分だけでノードごとの基本的な動作が伝わります

 その要素が何を行うものであるか判断できるよう、名前の先頭に目的に応じたプリフィクスを追加します。
 ルールに則ったプリフィクスを利用すると、ひと目でその要素が大雑把にどのような動作をするものか判断しやすくなり、メンテナンス性が向上します。

 例えばノード名はそのノードの動作に適した動詞をプリフィクスにします。
 変数名の場合も同様に、変数の役割がわかるようなプリフィクスをつけます。

 このようにしておけば、ノードリストなどでノードを検索する際のヒントにもなります。

目的の例プリフィクスの例
何かを追加するadd_
何かを削除するdel_
何かを計算するcalc_
何かを移動するmove_
何かをセットするset_
何かのテストにつかうtest_

・意味のある名前について

aaaaaaaaaaaは最終的に必要?目的は?
1年後に思い出せる気がしません・・・

 プログラミング学び初めの頃によくやってしまいがちなことの一つに、実験用の変数のような「瞬間的にほしい要素」に対し、一見して意味のない名前をつけるということがあります。

 これは、実験中はいいのですが、実験終了後にこの要素が不要となったにもかかわらず、要素を消し忘れたまま時間が経ってしまった時に問題を引き起こします。

 このような要素は本来不要であるにもかかわらず、なんとなくまだ必要そうな気がしてしまい、出来上がった仕組みを壊してしまうことに対する恐れも手伝い、消せないまま放置してしまったことがある方は多いのではないでしょうか。

 また、実験用に作った要素が最終的に採用され、利用されることになるケースもあります。
 このような場合も、はじめから意味のある名前をつけておけば、名前を修正する手間が省けて一石二鳥です。

意味のない名前の例
a b c
aaa bbb ccc
gngingaio miewaaaaaa

※ループカウンターで使われる[i,j,k]や、座標を表現する際に使われる[x,y,z][u,v,w]などは、それ単体では意味のないアルファベットですが、一般的な名前として広く認知されているので、意味を持っています。
そのため、使用しても何ら問題ありません。

・名前の記法について

各種記法で記述したノード名のサンプル

 プログラミングで使われる名前の記法には いくつか種類があります。
 よく目にする主な記法はだいたい以下の3通りだと思います。

スネーク記法
全部小文字
単語の区切りをアンダーバーで区切る
プログラミングでは主に変数名で使用
calc_cut_distance
point_num
prim_area
add_two_point
ローワーキャメル記法
名前の先頭は必ず小文字
単語の区切りごとに頭文字を大文字にする
主に変数名で使用
calcCutDistance
pointNum
primArea
addTwoPoints
アッパーキャメル記法
すべての単語の区切りごとに頭文字を大文字にする
プログラミングでは主にクラス名で使用
CalcCutDistance
PointNum
PrimArea
AddTwoPoints

 筆者は最近、Pythonのコード規約であるPEP8にならうことが多いです。
 具体的に言うと以下のようなルールに従っています。

・ノード名やプログラムの変数名はローワーキャメルケースで記述
・Pythonコード中のクラス名はアッパーキャメルケースで記述

・略称について

略しすぎると、そのノードの役割が全くわからなくなります

 名前付き要素に略称を使うことは可読性を損なう一因となります。
 メンテナンス性を高めるためには、極力名前のパーツに略称を使わないことが望ましいといえます。

 例えば、あるノードを「calc_center_position」という具体的な名前にする場合を考えます。
 このとき、「position」という言葉が長すぎると感じるかもしれません。
 そのような場合、略称を使いたくなります。

 しかし、ここで「position」を「p」と極端に略した場合、その「p」がどのような意味合いで選択された名前なのか第三者には瞬時に理解できません。(もちろん前後の文脈から想像はできますが、瞬時に理解するのは難しいでしょう)
 もしかしたら、数日後の自分ですらその「p」が何なのか即答できないかもしれません。

 そこで、この「position」という文字列を、一般的によく使われる「pos」という略称に置き換えるのは悪くない考えです。
 ただし、前後の文脈によっては「pos」から「positive」など、別の言葉を連想する可能性もあり得るので、意味をはっきり伝えるという意味では少し確実性に欠けます。

 とはいえ名前が長くなりすぎるのも困りものです。
 実際のところ、若干の読みやすさを犠牲にして、明らかに一般的な共通認識として定着している略称のみを必要最小限の範囲で使用するのが良い落とし所となるでしょう。

元の名前の例略称名略称名(悪い例)
positionposps, p
pointptp
attributeattra, at,
numbernumm, nm,

・長すぎる名前について

長過ぎる名前は、詳細はわかりますが不便です
適度に略称を取り入れながら、ほどほどの長さに収めましょう

 名前の文字数は長くても20文字~30文字までといった意見をよく見かけます。
 確かに長い名前はコードが複雑化すると全体が見づらくなる原因になります。
 ちょっとした文章のような長さの名前はおすすめできません。

 ですが、省略しすぎて意味が全くわからない名前よりは遥かに良いです。
 短すぎる名前を使うくらいなら、意味のわかる長い名前を使いましょう。


 VEXやPythonの場合、Visual Studio CodeやSublimeなどに、対応する言語のプラグインをインストールして外部エディタとして利用すると、インテリセンス(コード補完機能)が使えるようになります。
 Houdini内ではノードパスなどでインテリセンスが利用できます。

 そのため、多少長い名前を使用していても間違いなく簡単に入力できるうえに、いざ名前が長すぎたと感じる場合でも、エディタの機能で特定の対象だけ簡単にリネームできるので、さほど問題になりません。

・名前付けの具体例

 上記を踏まえた上で、具体的な名付けの例を挙げてみます。

要素の目的や動作名前の例
ポイントを追加するwrangle名add_point
primにsizeアトリビュートを追加するwrangle名add_size_attr_to_prim
三角ポリゴンを削除するwrangle名del_triangle_prims
ループ数を格納する変数名loop_num
ポイント数を格納する変数名point_num
ベクトルAとBの内積を格納する変数名
dot_A_B

□アルゴリズムの更新とテスト

compare_geometryでunittestを行いながら処理を分解

 リファクタリングの重要なポイントに、処理の内容を変えずに構造を整理するということを挙げました。
 そのためには、リファクタリングの前後で結果に差がないことを確認しながら作業をすることが重要です。

 通常のプログラミングでのリファクタリングは以下のように進めます。

1リファクタリング対象を複製し、もとのロジックをとっておきます。
2リファクタリング後の処理がどうなっていれば正解なのか判断できるデータを作ります。
このデータを、リファクタリング後の処理結果と常時比較しながら、結果が異なる場合に即座に検知するために使います。
この比較は、一般的にユニットテストと呼ばれます。
3リファクタリング語の処理結果が、リファクタリング前と同じであることを確認しながら構造の修正を行います。
4リファクタリングが完了し、もとの処理が必要なくなったら削除

 Houdiniの場合もこれによく似ていて、以下のように進めます

1リファクタリング対象のノードや一連のノードチェーンを複製、分流。
2Switch SOPを作成し、分流したノードの出力を接続します。
このSwitch SOPでリファクタリング前後の結果を簡単にスイッチして確認しやすくします。
ジオメトリのアトリビュートを比較するユニットテスト用HDAを作成してもいいでしょう。
3リファクタリング前後で結果が同じであることを確認しながら構造の修正を行います。
4リファクタリングが完了し、もとの処理が必要なくなったら削除

□関心の分離

1つのWrangleに3種類の処理が含まれていたので分解した例

 1つのノードでは、1度に1種類の処理のみを行うようにします。
 例えば、色を設定するWrangle SOPでは色を設定することのみを行うようにします。
 このWrangle SOPでは、色を設定すると同時に法線を設定するように複数種類の処理をさせることはしません。
 色の設定と別に法線を設定する必要がある場合、2つの処理を切り分けて別々のノードを作成し、それぞれで処理を行います。

 処理を切り分けることでその処理が影響する範囲が小さくなります。
 これにより、この処理に対する何らかの変更が必要な場合に気を配る必要にある範囲が小さくなります。
 これが「関心を分離する」ということです。

 関心を分離した結果、それぞれの処理に変更を加えたい場合や、ある処理の前後に新たな処理を追加する場合に、変更すべき箇所がわかりやすくなります。

 また、ノードを切り分ける場合は、処理のステップが複数のノードに分かれるので、各ノードのVisibilityフラグを切り替えながら、各処理ごとの結果が確認しやすくなります。

 Wrangleの場合、コード中の特定の行で処理を止めて途中経過を確認するようなデバッグ機能がないので、Wrangleコードとそのコードで使われるパラメータを機能単位のWrangleに切り分けて関心を分離することになります。


□処理の集約

整理された5つの処理を、1つのsubnetに集約した

 Houdiniに限らず一般的なプログラミングでは、複数の小さな処理を積み重ねて大きな処理を組み立てていきます。

 Houdiniでは、複数のノードをSubnetノードにパックしてまとめることができます。
 複数の小さな処理で成り立つ大きな処理をパックすることで、処理のくくりが明確になり、ノードネットワークの見通しが良くなります。

 これは一見すると、上で挙げた「関心の分離」に反するように見えます。
 たしかに、何も考えずにSubnetに複数のノードをまとめてしまうと、むしろノードネットワークが俯瞰しづらくなり、混乱の原因になります。

 そのため、処理を集約する際は、必ず集約する処理に対し関心の分離を適用します。
 複数の処理を一つのSubnetにまとめるタイミングは、関心の分離を適用する前でも後でも問題ありません。
 関心の分離を適用するとノードが増えます。
 すでにノードネットワーク全体が膨大なノード数で構成されていて、これ以上ノードを増やすと視覚的に全体を俯瞰しづらくなる場合などは、先に関心の分離を適用する処理の範囲をSubnetにまとめておき、そのSubnet内で適用することで、その他の箇所に気を取られずに作業できるようになります。

 これもまた、ノードネットワーク全体を巨大な処理と見立てた関心の分離とも呼べます。


□処理の再利用

 一心不乱にノードネットワークを作成していると、途中で行ったコピー&ペーストの影響なども手伝い、全く同じ処理を随所で繰り返している事態に陥ることがあります。

 全く同じ処理が複数箇所にあり、それぞれの処理に同じ変更を加えたい場合、それぞれに対し個別に同じ変更を加えるのは現実的とは言えません。

 このような場合、処理を切り出して再利用できるようにします。
 その後、切り出した再利用可能な処理で各所の重複部分を置き換えることで、その後の処理変更が1箇所で行えるようになります。

・HDA

 Subnetにまとめた仕組みは、デジタルアセットに変換することで再利用が簡単になります。(この方法については、多くの方が言及されているので、ここでは解説しません)

・Compiled Block/Invoke Compiled Block

同じ処理の一つCompiled Blockにまとめ
Invoke Compiled Blockによって呼び出しながら再利用

 Compiled Blockに対応しているノードだけで処理を構成する必要がありますが、Compiled Blockを使って一連の処理をひとくくりにし、Invoke Compiled Blockを通して、一連の処理を再利用できます。


 他にも多くのリファクタリングテクニックがあるのですが、ここでは基本的なものに絞って紹介させていただきました。
 タイムリミットも迫り記事も長くなってしまったので、より実践的なリファクタリングの実例紹介は別の機会に・・・

 この記事について、なにかご質問や間違いがありましたら遠慮なくツッコミをいただけると助かります。


 最後に、個人的にリファクタリング関連で参考になったと感じる書籍をいくつか紹介します。

 Houdiniはそもそもプログラミングツールではないため、これらの書籍で紹介されるすべてのテクニックが適用できるわけではありませんが、多くの点で役立ちます。(今回ご紹介したリファクタリングや、Wrangle、Pythonでコーディングする際など)

 特に「レガシーコード改善ガイド」の方は、古いコードのメンテナンスに際し起こりうる様々な問題ごとに適用できる、リファクタリングとテストを使うコード改善テクニックが紹介されています。
 今回紹介したユニットテストを使う安全な開発方法(テスト駆動開発)も、こちらの書籍から着想を得ています。

 興味のある方はぜひ読んでみてください。


 

Houdini – Python(HOM)によるパラメータ操作

 前回は、HOMによりノードを選択する操作に関する記事を書きました。
 ノードを探して選択するだけではあまりにも寂しいので、今回はそれに引き続き、ノードのパラメータを操作する部分に関するメモを書き残したいと思います。
 Houdiniでは、ノードパラメータへの操作パスが幾通りも存在しており、とても柔軟な作りになっていますが、今回は、自分がよくやる方法を取り上げます。
「もっと便利な方法があるよー」などの情報があればぜひ教えてください。
 ここで紹介する各種メソッドはあくまでも一例で、実際には更に多くのメソッドが用意されているので、ぜひマニュアルも参照してください。
http://www.sidefx.com/docs/houdini/hom/hou/Node.html
http://www.sidefx.com/docs/houdini/hom/hou/Parm.html
http://www.sidefx.com/docs/houdini/hom/hou/ParmTuple.html


パラメータ名の確認

 まず最初に、パラメータへアクセスする際はパラメータ名が重要になるので、名前の確認方法から。

Parameter Spread Sheetで確認

 Parameter Spread Sheetで、ノードとパラメータをツリー表示で確認できる。
 各項目名の右端にある、カッコで囲まれた名前が実際にアクセスする際に使用するパラメータ名。パラメータにはUniform Scaleのような単一の値を持つパラメータと、位置や回転など、複数の値をひとまとめにして持つ配列パラメータがある。
 下図を例にすると、Axis Divisionsという「ラベル」が付けられたパラメータは、実際の名前が「divrate」であり、3つの子要素(divratex/y/z)を持っている配列パラメータである事がわかる。

ドラッグ&ドロップで確認


 Python Source Editorに、パラメータラベルをドラッグ&ドロップすることで正しい配列パラメータのパスと名前が確認できる。

ポップアップウインドウで確認

 Parametersパネルなどでパラメータ名ラベルの上にマウスカーソルを置き、少し待つと、パラメータ情報がポップアップで表示される。この中の、Parametersの部分にある文字列が、このノードにおけるパラメータの正式な名前になる。

↑↑↑
 この場合、tx ty tz がパラメータ名であると確認できるが、あくまでも配列要素の単一パラメータ名のみがわかる。
 正確な親の配列パラメータ名が知りたければ、Parameter Spread Sheetを確認するなど一手間必要。(大体の場合、配列要素パラメータ名の末尾を削ったものが配列パラメータ名になっているっぽい)


おおまかなパラメータアクセスの方法

パラメータへのアクセス方法

  1. パスを指定して直接パラメータオブジェクト(hou.Parm)へアクセス
  2. 任意のノードのパラメータオブジェクト(hou.Parm)へ、名前を指定してアクセス

パラメータの操作方法

  1. ノードオブジェクト(hou.Node)を直接操作
  2. パラメータオブジェクト(hou.Parm)を通して操作

単一パラメータオブジェクトの取得

import hou

# パラメータオブジェクトを直接取得
parm_direct = hou.parm('parm_path')

# ノードオブジェクトからパラメータ名を使用してパラメータオブジェクトを取得 
node = hou.node("node_path")
parm_from_node = node.parm('parm_name')

配列パラメータオブジェクトの取得

 translateやrotateのような複数要素で構成されるパラメータは、配列パラメータとしてまとめて各要素へアクセスすることができる。
 基本的に、これは単一のパラメータへのアクセスと同じように行うが、その際は、parmTupleやevalParmTupleといった配列パラメータを扱う専用のメソッドを使う。

import hou

# 配列パラメータオブジェクトの子要素配列を取得
tupleParm_direct = hou.parmTuple('tupleParm_path')

# 配列パラメータの値を取得
# ノードオブジェクトをつかまえておく
node = hou.node("node_path")
tupleParm_from_node = node.parmTuple('tupleParm_name')

# 子要素へは、tupleParmの配列要素へアクセスして行う
tupleParm_x = tupleParm_from_node[0]
tupleParm_y = tupleParm_from_node[1]
tupleParm_z = tupleParm_from_node[2]

単一パラメータの値を取得する

 eval~と名づけられたメソッドを使う。
 型を指定して型変換しながら値を取得するメソッドや、時間指定で値を取得するメソッド、それらを組み合わせて取得するメソッドなどもある。

import hou

# 単一パラメータオブジェクトから現在の値を取得
parm = hou.parm("parm_path")
parmValue = parm.eval()

# 型を指定ながら現在の値を取得
parmIntValue = parm.evalAsInt()
parmFloatValue = parm.evalAsFloat()
parmNodeValue = parm.evalAsNode()

# 指定時間での値を取得
parmAtFrameValue = parm.evalAtFrame( FRAME_NUMBER )

# 型指定と時間指定の組み合わせ
parmIntAtFrameValue = parm.evalAsIntAtFrame( FRAME_NUMBER )
parmFloatAtFrameValue = parm.evalAsFloatAtFrame( FRAME_NUMBER )

配列パラメータの値を取得する

 配列パラメータの値取得も、単一パラメータとほぼ同様。

import hou

# 配列パラメータオブジェクトから現在の値を取得
tupleParm = hou.parmTuple("parm_path")
tupleParmValue = tupleParm.eval()

# 型を指定ながら現在の値を取得
parmIntValues = tupleParm.evalAsInts()
parmFloatValues = tupleParm.evalAsFloats()
parmNodeValues = tupleParm.evalAsNodes()

# 指定時間での値を取得
parmAtTimeValue = parm.evalAtTime( TIME )
parmAtFrameValue = parm.evalAtFrame( FRAME_NUMBER )

# 型指定と時間指定の組み合わせ
parmIntAtFrameValues = parm.evalAsIntsAtFrame( FRAME_NUMBER )
parmFloatAtFrameValues = parm.evalAsFloatsAtFrame( FRAME_NUMBER )

パラメータをセットする

 値のセットは、set系メソッドを使う。
 事前に組み立てたパラメータ辞書をsetParmsメソッドに与え、一括で値をセットするのがとても楽なのでおすすめ。
 その際、setParmsメソッドでは配列パラメータ名は認識できないので、各要素を個別の単一パラメータとして辞書に値を用意します。

import hou

# 単一パラメータに値をセットする
parm = hou.parm('parm_path')
parm.set( value )

# 配列パラメータにシンプルな値の配列をセットする
tupleParm = hou.parmTuple('parm_path')
tupleParm.set( (value1, value2, value3, ...) )

# ノードオブジェクトを使い、複数のパラメータをまとめてセットする
# セットしたいパラメータを辞書に溜め込み、setParms()に与える
parmDict = { 'parm_name1':value1, 'parm_name2':value2, ... }
node = hou.node('node_path')
node.setParms(parmDict)

 以上、何か不明点などあれば気軽にご質問ください。
 間違いがあればツッコミも大歓迎です。

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つ上流のノードを探し、再度全下流ノードを探して全選択を行うなどの便利なコマンドを作成できます。

CGWORLD v.230 に寄稿させていただきました

久しぶりの更新が宣伝というのもアレですが・・・

2017/09/08発売 の CGWORLD v.230 に掲載の特集記事「すぐに役立つTIPSとアプローチ ワンランク上のキャラクターリギング」に、株式会社コロッサス名義で「ダイナミックコントロールリグのためのツール活用術」というタイトルの記事を寄稿させていただきました。

地味に手間のかかる馬の手綱セットアップを例として、ダイナミックリグの作成方法と、ツールによって作業を簡単化する方法を紹介しています。

また、スクリプトは書いたことがあるけれど、APIまでは手を出したことがないという方向けに、Maya Python API 2.0によるOpenMayaの基本的な使い方の解説と、メッシュ上の最近接点の取得や、カーブ上の一点が持つ各ベクトルからのマトリクス組み立てといった、覚えておくと色々な場面で役に立つ内容も盛り込んでみました。

また、現在在籍している「株式会社コロッサス」のブログにて、ツールの配布等もしておりますので、ぜひ本誌を手に取っていただけると幸いです。

<株式会社 コロッサス:ブログ CGWORLD v.230
<CGWORLD SHOP:CGWORLD v.230

Interactive Houdini

Houdini Advent Calendar 2016 18日目に投稿させていただいた記事です


■はじめに

Houdiniには外部デバイスからインタラクティブにデータを入力する機能があります。

今回は、その機能を利用して外部に接続した各種フィジカルデバイスの操作から、Houdiniへデータを入力する方法を色々と書いてみたいと思います。

手頃に試せそうな例として、ここでは以下のデバイスからの入力を試してみます。

・マウス
・ペンタブレット

・キーボード
・MIDIコントローラ


■環境

・Windows10 64bit
・Houdini Indie 15.5.673


■まずは今回取り上げるCHOPに共通する予備知識

今回取り上げるCHOPは以下の通りです

Mouse CHOP
Keyboard CHOP
MIDI In CHOP

・CHOPデータの在り処について

CHOPのデータは、「CHOPチャンネル」が持っています。
CHOPチャンネルのデータへアクセスする際は、CHOPノードだけではなく、CHOPノード内にあるCHOPチャンネル名まで指定する必要があります。

・CHOPデータの詳細確認

CHOPノードを中ボタンでクリックするとCHOP内のチャンネル数や、開始/終了フレーム、サンプルレートなどの詳細情報が表示されます。

赤く囲んだ部分には、このCHOPに含まれるチャンネル名と値が表示されています。
この例の場合、ステレオのオーディオファイルを読み込んでいるので、このCHOPには左右2チャンネルの波形データが含まれていることがわかります。
そして、チャンネル名は左右でそれぞれ chan0 chan1 が割り当てられています。

・CHOPチャンネルへのパス

CHOPチャンネルへのパスは、CHOPノードへのパス+CHOPチャンネル名で指定します

~/chop_name/channel_name

上記の例で言うと、WAV_FILE(File CHOP) の chan0 チャンネルへのフルパスは以下のようになります

/obj/driver_chopnet/WAV_FILE/chan0

・CHOPチャンネルへのアクセス

CHOPチャンネルの各情報は、chop系エクスプレッション関数を使って取得します。
これらの詳細はマニュアルを参照してください。

  • chop : 現在時間でCHOPチャンネルの値を取得
  • chope : CHOPチャンネルの終了インデクスを取得
  • chopf : 指定フレームでCHOPチャンネルの値を取得
  • chopi : 指定サンプルポイントでCHOPチャンネルの値を取得
  • chopl : CHOPチャンネルの長さをサンプル数で取得
  • chopn : CHOP内のチャンネル数を取得
  • chopr : CHOPのサンプルレートを取得
  • chops : CHOPチャンネルの開始インデクスを取得
  • chopt : 指定した時間でのCHOPチャンネルの値を取得
  • chopcf : 指定フレーム、指定チャンネルインデクスでCHOPチャンネルの値を取得
  • chopci : 指定サンプルポイント、指定チャンネルインデクスでCHOPチャンネルの値を取得
  • chopct : 指定時間、指定チャンネルインデクスでCHOPチャンネルの値を取得
  • chopstr : 現在時間でのCHOPチャンネルの文字列データを取得

先の例で登場したCHOPチャンネルが持つ現在時間のデータが欲しい場合は、データを呼び込むパラメータボックス内で以下のエクスプレッションを使用します。

chop( “/obj/driver_chopnet/WAV_FILE/chan0” )

・CHOPチャンネルの波形データを確認する

CHOPの波形データは、CHOPノードの右端にある[Graphフラグ]をオンにすることで、Motion FX Viewに表示されます。

Motion FX View

・CHOPデータのサンプルレート

CHOPでは様々な波形データを扱うことができます。
波形データは自由に再生時のサンプルレート(一秒あたりのデータ数)を指定できるようになっています。

サンプルレートの高いオーディオデータを30fps設定のシーンでアニメーションカーブとして使う場合、サンプルレートを30にリサンプルしてから使用するのが負荷軽減につながって良いのではないかと思います。

サンプルレートは、File CHOPのChannelタブ内またはResample CHOPにあるSample Rateパラメータで指定します。

・上で読み込んだWAVデータの波形を30fps化したところ

・外部から入力されたデータのレコーディング

外部から入力されたデータを記録しておき、再生することができます。
MIDIデータを外部から入力するMIDI In CHOPなどは、入力されたMIDIデータを自分自身に記録する事ができるようになっていますが、Record CHOPを下流に接続してそちらで記録するほうがデータの取り回しの面で良いと思います。

レコーディングは、データ入力CHOPの下流に Record CHOPを接続し、Record パラメータをオンにした状態で行います。


■というわけで、早速やってみよう

CHOP入力→レコーディング→再生の流れは、今回紹介するどのCHOPでも変わらないので、まずはMouse CHOPの簡単なチュートリアルで作業の流れを掴んでみます。


■Mouse CHOPチュートリアル

1・CHOPコンテキストにCHOP Networkを作成します

2・CHOP Network の中に Mouse CHOPを作ります

3・Mouse CHOPのGraphフラグをオンにしたあと、Motion FX Viewを表示します

4・Mouse CHOPの下にRecord CHOPを接続します

5・Record CHOPのRecordパラメータをOnにし、シーンを再生します

この時、シーンのリアルタイム再生モードをオンにしておくと実際のフレームレートでレコーディングできます。

また、再生モードを Play Onceにしておくことでリピート再生をオフにし、うっかり開始フレーム付近を書き換えてしまうのを防げます。

6・マウスカーソルを自由に動かし、グラフが描かれていくのを確認します


各カーブの上には、対応するカラーでチャンネル名が表示されています。(geo1:txgeo1:ty

7・記録できたら、Record CHOPのRecordをOffにします

8・/obj/に新たにBoxオブジェクトを作ります

9・boxオブジェクトのTranslateX/Yにエクスプレッションを設定します

・TranslateX
chop(“../../ch/ch1/record1/geo1:tx”)

・TranslateY
chop(“../../ch/ch1/record1/geo1:ty”)

10・シーンを再生して結果を確認します

boxオブジェクトが、マウスが動いたとおりに移動するのが確認できます。

11・ペンタブレットをお持ちの場合

Mouse CHOPのUse Tabletをオンにして、同様の操作をすることで、筆圧やペンの傾きなどを検知し、記録できる事がわかると思います。
Pressure CHOPチャンネルを boxオブジェクトのUniform Scaleパラメータにアサインすると楽しいかもしれません。


■データ入力手順のまとめ

これが外部入力系のCHOPを使うワークフローの基本的な手順です。
手順をまとめると以下のようになります。

1・外部入力を受け付けるCHOPを作成
2・CHOPにデータの窓口となるCHOPチャンネルを作成
3・任意のパラメータからchop関連のエクスプレッション関数でCHOPチャンネルのデータを参照する

これはその他のデータ入力CHOPを使った場合も同様です。
これだけ理解できれば、あとは応用して色々できるはず。


■Keyboard CHOPチュートリアル

Mouse CHOPに続いて、Keyboard CHOPチュートリアルです

1・CHOPコンテキストにCHOP Networkを作成します

2・CHOP Network内にKeyboard CHOPを作成します

3・Keyboard CHOPの設定をします

Name 1

どんな名前でもいいのですが、ここでは [key_a] と記入します。

Type 1

そのままにします。

Type 1の右側にあるプルダウンメニュー

 [A] を選択します。

同様に、Name 2~Name 4まで、それぞれ同様に s,d,w キーを割り当てます。


4・Interceptモードをオンにします

キーボード上にあるScroll Lockキーを押し、Interceptモードをオンにします。

現在時間がオレンジ色になります。

5・キーを押して反応を確かめます

 a,d,wを同時押しした様子

あとは、Mouse CHOPのチュートリアルと同じ方法で、下流にRecord CHOPを繋いでレコーディングできます。

Interceptモードではキーボード入力が無視されるのでマウスでシーンの再生ボタンを押してください。

chopエクスプレッション関数による値の取得も同様に行えます。


■MIDI In CHOPチュートリアル

最後に、MIDI In CHOPを使用してMIDIコントローラから入力するチュートリアルです

1・CHOPコンテキストにCHOP Networkを作成します
2・CHOP Network内にMIDI In CHOPを作成します

3・MIDI In CHOPの設定をします

・Sourceタブ

MIDI Source : MIDIIN2 (Launchpad Pro)
MIDI Channels : 1

※MIDI Sourceは、各々がお持ちのMIDIデバイス名を選択してください。

・Noteタブ

Note Scope : 0-127
Aftertouch Name : af

4・鍵盤を叩き、反応を確かめます

・ch1n60 (音階で言うとど真ん中の[ド])を押したところ

今回使用したLaunchpad Proはアフタータッチ対応のMIDIコントローラなので、打鍵後に鍵盤を更に押しこむ事でafチャンネルを変化させることができます。

あとは、Mouse CHOPのチュートリアルと同じ方法で、下流にRecord CHOPを繋いでレコーディングできます。

chopエクスプレッション関数による値の取得も同様に行えます。


■おわりに

今回ご紹介したテクニックを使うことで、直感的なデータ入力ができるようになります。

例えば、特定のキーを押したタイミングでパーティクルを発生させたり、ジオメトリの頂点カラーを変化させたり、キーボードを叩くたびに乱数のSeed値を変化させて、次々と予想だにしないビジュアルを作り出すこともできるでしょう。

また、キーを押す強さが強いほどパーティクルの発生量を多くしたり、色を濃くしたりと言った、複雑な表現もできるようになります。

Joy To Keyのようなツールを併用することで、手に馴染んだゲームコントローラを操作してHoudiniへのデータ入力を行うことも簡単にできます。

また、今回はNetwork CHOP や Pipe In CHOPは使用しませんでしたが、これらも使いこなせばなかなかに楽しいことができそうなCHOPなので、いずれ折を見て記事にしたいと考えています。

ここまで読んでいただき、ありがとうございました。
何か不明点や間違いがあれば、記事へのコメントやTwitterなどで質問、ツッコミください。


■おまけ

以降は、今回使用したCHOPのマニュアルみたいなものです。


■マウスからの入力 : Mouse CHOP

マウスのカーソル位置を取得することができます。
タブレットを使用している場合は、筆圧やペンの傾きなども取得できます。

・PositionX/Y

ここで指定した文字列が、それぞれに対応するCHOPチャンネル名になります。
画面左下から右上に向かって値が増えていきます。
Xの範囲は-1~1、Yの範囲は-0.8~0.8

・Use Tablet

オンにするとPressureやAngleなど、タブレット向けパラメータが有効になります。
PositionX/Yと同様、ここで入力した各文字列が、それぞれに対応するCHOPチャンネル名になります。


■キーボードからの入力 : Keyboard CHOP

指定したキーが押されたとき、Typeで指定した方法で値を入力します。

このノードを動作させるためには Interceptモード をオンにする必要があります。
Interceptモードをオンにするには、Scroll Lockキーを押しScroll Lockを有効化します。

Interceptモードが有効になると現在フレームがオレンジ色に着色されます。
この状態でキーイベントを取得するよう設定したキーを押すことで、値が入力されます。

・Modifier Keys

使用する修飾キーを指定します。

上記画像の例では、[A]を押している間だけCHOPチャンネル[a1]の値が1になります。

・Name

対応するキーが押されたときにオンになるCHOPチャンネル名を指定します。

・Type

キーを押したときの動作

Momentaly:押しているときだけ1を送出
Toggle:押すたびに0と1を切り替え
Count:押すたびに1ずつ値が増える
Pulse:押した瞬間だけ1を送出
Time:シーン再生中にキーが押され続けた時間

・Key

このチャンネルに値を入力するキーを選択


■MIDIデバイスからの入力:MIDI In CHOP

MIDIキーボードやフェーダーコントローラなどを使用して、Houdiniに外部から直接MIDIデータを入力することができます。
また、MIDIファイルを直接読み込む事もできます。

・Sourceタブ

MIDI Source

MIDI信号の入力ソースを指定します。
MIDIファイルやMIDIデバイスを選択できます。
インタラクティブな入力を行いたいなら、接続済みのMIDIデバイス名を選択します。
すでに作られた楽曲などを入力したいならMIDI Fileを選択します。

MIDI Channels

入力するMIDIチャンネルの番号を指定します。

入力するMIDIチャンネル数が多いと入力データの転送に時間がかかります。
例えば、使用するノート番号の範囲が0-127の場合、使用するMIDIチャンネル1つごとに128個のデータを解釈することになります。
そこでアフタータッチをポリフォニックモードで解釈させるとなると、さらに128倍の負荷がかかります。
そのため、インタラクティブに操作したい場合は入力するMIDIチャンネルを一つに絞ったほうがいいと思います。
複数のMIDIチャンネルを入力するのは、MIDIファイルを入力する時のみで良いでしょう。

入力を許可するMIDIチャンネル番号は以下のように指定できます

・スペース区切り(個別指定)

1 2 10 11

・ハイフンでつなぐ(範囲指定)

1-10

Channel Prefix

入力するMIDIチャンネル番号を識別するために付加される接頭辞を指定します。
ここで指定した文字列のあと、入力されたMIDIチャンネル番号が付加されます。
この値が[ch]の時、CHOPチャンネル名は以下のような書式になります。

ch1
ch2

Echo Messages to Textport

オンにすると、受信したMIDIメッセージの内容がHoudini Consoleに表示されます。
デバッグに便利なので必要に応じてオンにします。

・Recordタブ

レコーディングには、MIDI In CHOPの下流に接続するRecord CHOPを使用するので、ここでは何も変更しません。

・Noteタブ

主にMIDIコントローラの鍵盤部分とピッチベンドコントローラからの入力信号の解釈方法を決定するタブです。

・チャンネル名パラメータ

チャンネル名パラメータは、対応するMIDI信号を受けとるCHOPチャンネルを作成します。

Note Name : ノートナンバー
Velocity Name : ノートのベロシティ
Aftertouch Name : ポリフォニックキープレッシャー
Pressure Name : チャンネルプレッシャー
Pitch Wheel Name : ピッチベンド

ここで指定した文字列が、MIDIチャンネルを示す文字列のあとに続きます。
例えば、Note Nameの値が[n]なら、MIDIチャンネル1のノート番号60の値を受け取るCHOPチャンネル名は以下になります。

ch1n60

これらのCHOPチャンネル名パラメータに何か文字列が入力されると、その時点でCHOPチャンネルが作成され、データの入力待ちが開始されます。
使用しないMIDI信号の入力については負荷軽減のため何も入力しないのがベターです。

Note Scope

入力を受け付けるMIDIノートナンバーの範囲を指定します。
0-127の範囲が使えます。
Sourceタブの MIDI Channels と同様の書式で指定できます。

Note Output

・One Multiplexed Channel

ノート信号を番号ごとに分けず、一つのチャンネル内で押されたノート信号をすべてまとめて使用します。

この時、CHOPチャンネル名にノート番号は付かず、以下のような名前になります。

ch1n : チャンネル1のノートすべて

・Separate Channels

ノート信号をノート番号ごとに個別のチャンネルとして入力します。
この時、CHOPチャンネル名は以下のようになります。

ch1n60 : チャンネル1のノート番号60番
ch1n127 : チャンネル1のノート番号127番

Velocity

MIDI Note信号のVelocityをどのように受け取るか指定します。

・Off

Velocityを入力しません
ノートを入力するCHOPチャンネルの値はオンオフの2値になります。

・Note Amplitude

ノートを入力するCHOPチャンネルの値が、Velocityの値になります。

・Separate Channels

Note信号の値はオンオフの2値になります。
同時に、Velocityは次のVelocity Nameで指定された接頭辞とノート番号で表されるCHOPチャンネルの値として取得されます。

Normalize

入力されるVelocityデータの値の範囲を指定します。

・None

値は 0-127 の範囲で入力されます。

・0-1

値が 0-127 を 0-1 にマッピングした状態で入力されます。

・Controlタブ

MIDI CCメッセージに関する解釈方法を決定するタブです。
MIDIキーボードにスライダやツマミが付属している場合がありますが、そのような鍵盤以外のコントロールから送信されるデータに関する入力設定です。

・チャンネル名パラメータ

チャンネル名パラメータは、対応するMIDI信号を受けとるCHOPチャンネルを作成します。

Controller Name : CCメッセージ
Program Change : プログラムチェンジメッセージ

Controller Type

一般的なMIDI音源やDAWなどで共通してMIDI CCメッセージへ割り振られているコントロールを、わかりやすくプルダウンメニューから指定できます。

Controller Index

取り扱うMIDI CCメッセージのコントロールナンバーを直接指定します。
0-127の範囲が使えます。
Sourceタブの MIDI Channels と同様の書式で指定できます。

Controller Format

・7 bit Controllers

扱うCCメッセージがLSB/MSBに対応していない場合はこちらを選択します。

・14 bit Controllers

扱うCCメッセージがLSB/MSBに対応している場合はこちらを選択します。

Normalize

入力された値の範囲をリマップします。

・None

リマップしません

・0 to 1

0~1にリマップします

・-1 to 1

-1~1にリマップします

・On / Off

ブール値化します

Unwrap

これはちょっとよくわかりませんでした。
ラベルから察するに、ターンテーブルのような値をインクリメント/デクリメントするコントローラ向けに、値が0-127の範囲外にセットされたときに値をループさせるか否かを決めるオプションのような気がします。

 ・Sysタブ

MIDIクロックメッセージやシステムメッセージに関する解釈方法を決定するタブです。
今回は、手元で検証できる環境がないので端折りますが、この辺をちゃんと設定すれば外部のDAWとHoudiniの間で内部時間や、再生操作や停止操作などを同期できるはず。

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

Ricoh THETA S でIBL画像を作成するためのスクリプトを書きました

簡易的なIBL用のHDRIを作成するため、素材になる天球マップ画像をRicoh THETA SをiPhoneで遠隔操作して手軽にオートブラケット撮影できるようにしてみました。

撮影方法は、ISOを固定しEVを変化させて行う露出ブラケット撮影になります。
今の所、使用する撮影モードではシャッタースピードを同時に操作できなそうなので、自動設定にしてあります。

THETA SはRAWデータでの撮影ができず、また撮影時に厳密なホワイトバランス設定などもできないので精度は落ちますが、簡易的に使用するにはそこそこ使えるHDRIが得られると思います。

iOSデバイスに限らず、THETA Sとネットワーク接続できるデバイスで、なおかつ標準的なPythonを実行できるなら同じように使用できるはずです。

というわけで、需要がありそうなのでソースコードを公開します。

時間の関係で良い作例の用意ができていませんが、そのうち貼りたいと思います。

・使い方

1:デバイスとTHETA Sをネットワーク接続接続します

2:下記スクリプトをデバイス内のPython実行環境で実行します
当方は、iPhone6S+上で iOS用のPython 2.7 というアプリを使用しています。

3:撮影されたJPEG画像をHDR ShopLuminance HDRなどを使ってHDRIにします

#******************************************************************************
'''
Tool Name     : MYAM_ThetaAutoBracket.py
Description   : 
Copyright     : (c) Yamabe Michiyoshi
Author Name   : Yamabe Michiyoshi
'''
#******************************************************************************

import httplib
import json
import time

# 
class ThetaSettings(object):
    # Full
    # evs = [-2.0, -1.7, -1.3, -1.0, -0.7, -0.3, 0.0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0]

    # Simple
    evs = [-2.0, -1.0, 0.0, 1.0, 2.0]

    exposureProgram = 9
    iso = 100
    whiteBalance = "auto"
    _shutterVolume = 50

    shutterInterval = 1

#
class Theta(object):
    def __init__(self):
        self.headers = {"Content-Type":"application/json", "Accept":"application/json"}

    # Auto Bracket
    def takePicturesByAutoBracket(self):

        print "--------------------"
        print "START AUTO BRACKET"
        print "--------------------"

        # Connect to THETA 
        connection = self.connect()

        # Start Session
        self.startSession(connection)

        # Get Session ID
        responseData = self.getResponseData(connection)
        sessionId = responseData["results"]["sessionId"]

        # Take Pictures
        for i in range(len(ThetaSettings.evs)):
            
            # Set options
            ev = ThetaSettings.evs[i]
            
            print "----------"
            print "%i / %i" % ((i + 1), len(ThetaSettings.evs))
            print "iso : %i" % ThetaSettings.iso
            print "EV  : %d" % ev

            picOpt = {"exposureProgram":ThetaSettings.exposureProgram,
                      "exposureCompensation":ev,
                      "iso":ThetaSettings.iso,
                      "whiteBalance":ThetaSettings.whiteBalance,
                      "_shutterVolume":ThetaSettings._shutterVolume}

            self.setOptions(connection, sessionId, optionParams=picOpt)
            responseData = self.getResponseData(connection)

            # Take Pictute
            self.takePicture(connection, sessionId)
            
            # Wait to finish idle time
            self.waitFinishCurrentCommand(connection, interval=ThetaSettings.shutterInterval)

        # Close Session
        self.closeSession(connection, sessionId)

        # Disconnect from THETA
        self.disconnect(connection)

        print "--------------------"
        print "FINISH AUTO BRACKET"
        print "--------------------"

    # HTTPConnection Control
    def connect(self):
        connection = httplib.HTTPConnection("192.168.1.1",80)
        return connection

    def disconnect(self,connection):
        connection.close()

    # Common Commands
    def postExecCommand(self,connection,params):
        connection.request("POST", "/osc/commands/execute", params, self.headers)

    def postStatusCommand(self, connection, commandId):
        params = json.dumps({ "id":commandId })
        connection.request("POST", "/osc/commands/status", params, self.headers)

    def waitFinishCurrentCommand(self, connection, interval=1):
        responseData = self.getResponseData(connection)
        commandId = responseData["id"]

        while True:
            self.postStatusCommand(connection, commandId)
            responseData = self.getResponseData(connection)
            commandStatus = responseData["state"]

            if "inProgress" == commandStatus:
                print "commandId [%s] : Processing..." % ( commandId )
                time.sleep(interval)
            else:
                print "commandId [%s] : Finish!" % ( commandId )
                break

    # Command Control
    def startSession(self, connection):
        params = json.dumps({ "name":"camera.startSession", "parameters":{} })
        self.postExecCommand(connection, params)

    def updateSession(self,connection,sessionId):
        params = json.dumps({ "name":"camera.updateSession", "parameters":{"sessionId":sessionId} })
        self.postExecCommand(connection, params)

    def closeSession(self, connection, sessionId):
        params = json.dumps({ "name":"camera.closeSession", "parameters":{"sessionId":sessionId} })
        self.postExecCommand(connection, params)

    def takePicture(self,connection , sessionId):
        params = json.dumps({ "name":"camera.takePicture", "parameters":{"sessionId":sessionId} })
        self.postExecCommand(connection, params)

    def setOptions(self, connection, sessionId, optionParams):
        params = { "name":"camera.setOptions" , "parameters":{"sessionId":sessionId, "options":{} } }

        for key , value in optionParams.items():
            params["parameters"]["options"][key] = value

        jsonParams = json.dumps(params)
        self.postExecCommand(connection, jsonParams )

    def getOptions(self, connection, sessionId, optionNames):
        params = { "name":"camera.setOptions" , "parameters":{"sessionId":sessionId, "optionNames":{} } }

        for key , value in optionParams.items():
            params["parameters"]["options"][key] = value

        jsonParams = json.dumps(params)
        self.postExecCommand(connection, jsonParams )

        self.postExecCommand(connection, params)

    # Support Functions
    def getResponseString(self,connection):
        response = connection.getresponse()
        responseString = response.read()
        return responseString

    def getResponseData(self,connection):
        responseString = self.getResponseString(connection)
        responseData = self.convertJSONToPythonData(responseString)
        return responseData

    def convertJSONToPythonData(self,jsonDataString):
        jsonDecoder = json.JSONDecoder()
        pythonData = jsonDecoder.decode(jsonDataString)
        return pythonData

    def convertPythonDataToJSON(self,pythonData):
        jsonEncoder = json.JSONEncoder()
        jsonDataString = jsonEncoder.encode(pythonData)
        return jsonDataString

def main():
    theta = Theta()
    theta.takePicturesByAutoBracket()

if __name__ == "__main__":
    main()

・設定

設定の変更を行うにはコードを直接書き換える必要があります。
撮影枚数は、ThetaSettingsの evs で指定されているEV値の数で指定されます。
撮影ごとにリスト内のEV値がそれぞれ使用されます。

・EV値と撮影枚数の設定

class ThetaSettings(object):
    # Full
    # evs = [-2.0, -1.7, -1.3, -1.0, -0.7, -0.3, 0.0, 0.3, 0.7, 1.0, 1.3, 1.7, 2.0]

    # Simple
    evs = [-2.0, -1.0, 0.0, 1.0, 2.0]

指定できるEV値は、上記の Full で指定されている13個の値になります。
THETA Sは一枚撮影するごとに内部処理に約8秒ほどかかるようなので、撮影枚数が多くなると時間がかかります。
必要に応じ、不要な値を削除するなどして調整してください。

・各種オプションの設定

    exposureProgram = 9
    iso = 100
    whiteBalance = "auto"
    _shutterVolume = 50

現在は、上記を指定できます。
必要に応じ、RICOH THETA API v2 Referenceを参照して値を設定してください。
といいつつおそらく上記の中では iso と _shutterVolume くらいしかいじらないと思います。

・_shutterVolume
0−100の間で指定します。0が無音です。

・iso
以下の値が使えます
100, 125, 160, 200, 250, 320, 400, 500, 640, 800, 1000, 1250, 1600

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

リンク

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

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