wren.png

ここでは、Wren ROPの使い方とSOHOによる外部アプリとのパイプの構築について説明します。
おおまかな流れは、

  1. Wren ROPを使ってHoudiniの3Dシーンから輪郭線やエッジをベクトルデータとして生成する
  2. Houdiniで作成したベクトルデータをドローソフトに読み込む
  3. HoudiniでポリゴンカーブをNURBSカーブに変換する
  4. SOHOを使ってHoudiniからFreeCADへのパイプを組み込んでNURBSカーブをSVGに変換する
  5. おまけ1(HoudiniからSOHOを介してMayaに接続する)
  6. おまけ2(MayaからHoudiniに接続する)

について解説しています。
尚、Houdini16.5の時点における解説ですので、バージョンによって内容が異なることがあります。
Wren ROP改良されないかなぁ:dancer_tone2:そんな期待を抱きながら、この記事を書いています。

トップのWren ROPのアイコンの鳥はwrenと呼ばれているスズメ科の鳥だそうです。
さて、需要のある内容なのかどうかわからないけど、他とかぶらなそうなネタを書きます。頑張ります:muscle:

Wren ROPとは?

HoudiniにはMantraレンダラーの他にもベクトルレンダラーのWrenが用意されています。
このWren("レン"と呼びます)は、Wireframe Rendererのことで、シーン内のオブジェクトのシルエットや尖ったエッジ(今後はカスプエッジと呼ぶことにします)をワイヤーフレームとしてレンダリングすることができます。
ベクトルレンダラーなので、レンダリング結果をベクトルデータとして出力することができます。

WrenはMantraと同様にRenderライセンスを消費するので、一度IFD(レンダリング用のシーン記述ファイル)ファイルを書き出してしまえばHoudiniライセンスを使わずに分散レンダリングさせることができます。
ここで注意しておかなければならないのは、Mantraが書き出すIFDとWrenが書き出すIFDはまったく別物ということです。
つまり、Wrenから書き出したIFDをMantraコマンドでレンダリングすることはできません。Wrenコマンドを使用します。
但し、Apprentice/IndieライセンスではIFDの書き出しは不可になっています。

まずは、テキトーにモデルを用意します。私の方はシェーダボール、ボックス、グリッドを配置しています。
SampleModel.png
Wren ROPの使い方はMantraと同様にネットワークエディタのoutコンテキスト内にWren ROPを追加します。
Wren ROPのアイコンって可愛いっすよね。チュンチュン:hatched_chick:
wrenROP.png
Wren ROPのデフォルトのままの挙動を見てみたいので、Wren ROPのパラメータエディタのRender to Diskボタンを実行します。するとMPlayerが立ち上がってレンダリング結果が表示されます。
defaultResult.png
シルエットには線が出てますが、カスプエッジ部分が出てませんね。
Mantraレンダラーなら法線アトリビュートからカスプエッジを認識できるのに対して、Wrenレンダラーは法線アトリビュートを見ません。
Wrenでは線幅を意味したlinewidthアトリビュートしか認識することができません。
それ以外のアトリビュートを認識しないので、カスプエッジとして認識させる場合には 頂点のポイントを分離 (ポイント数が増えてしまう)させる必要があります。

これは、Fuse SOPEdge Cusp SOPFacet SOPを使えば可能です。
以下はFuse SOPの"Unique"を有効にするか、またはEdge Cusp SOPをすべてのオブジェクトに適用してみた結果です。
FuseSOP.png
FuseResult.png
完全にポリゴンの輪郭が線として出たのが分かります。
カスプエッジの部分のポイントだけを分離させたい場合はFacet SOPを使ったほうが楽です。
FacetSOP.png
Cusp Polygonsのチェックを有効にして、Cusp Angleに閾値の角度を設定します。これはポリゴンの法線間の角度なので値が低いほど検出されるカスプエッジが増えます。
FacetResult.png
Mantraで塗りの絵を出して、Wrenで線だけの絵を出して、それをコンプすればトゥーンな絵を作ることができます。
その場合にはWren ROPのPropertiesタブのRenderサブタブのShading Modelblack wire/matte fill(線の色を黒、塗りつぶしなし)、好みに応じてLine Widthに線幅を指定します。
ShadingModel.png
Comp.png

Wren ROPの欠点は、

  • スムーズシェーディングに対応していないので、滑らかにするためにモデルを物理的に細分化する必要がある
  • ディスプレイスメントに対応していない
  • シングルスレッド
  • (Point Consolidationを有効にしていても)重複ポイントがちゃんと結合されない
  • "N"や"creaseweight"といったアトリビュートが反映されない

が挙げられます。
ただどうしても
線は滑らかにしたい。ポリゴンカーブなんていやだ。
重複ポイントは結合したい。
ので、Wrenから曲線ジオメトリを書き出してHoudiniや他のドローソフト(IllustratorとかInkscape)でそれを編集できるようにしたいと思います。

WrenからPostscriptを書き出す

Wren ROPのPropertiesタブのOutputサブタブのOutput Pictureに拡張子を.epsにして出力ファイル先を指定します。Postscriptの拡張子は通常は.psなのですがHoudiniでは.epsにしないとファイルを読み込むことができないので注意してください。
Wren ROPから出力されるPostscriptのフォーマットはPostScript Level 1です。
そして、
Output Postscriptのチェックを付けるか
EnablePostScript.png
または、MainタブのCommand-pオプションを指定します
Commandline.png
のどちらかの方法でPostscriptを書き出すことができます。
前者だと出力されるPostscriptのサイズ(ページサイズのことです)を指定することができないので、
サイズを明示的に指定したい場合には後者の方法を使って-w(幅),-h(高さ)を指定します。

IllustratorやInkscapeで読み込み可能なPostscriptファイルを作成する

InkscapeはデフォルトではPostscriptファイルを読み込むことはできません。
事前にGhostscriptをインストールし、Pathシステム環境変数をC:\Program Files\gs\gs9.22\bin;C:\Program Files\gs\gs9.22\libのようにGhostscriptのbinとlibのパスを追加しておく必要があります。

Commandパラメータに指定するwren -p -w 幅 -h 高さ
幅と高さの単位はインチです。
Postscriptの1単位は1インチ/72です。
-w-hのオプションを指定しなかった場合は、カメラ解像度のサイズ、例えば1280x720のカメラならページサイズが1280インチ、720インチのPostscriptが定義されてしまい、これはIllustratorでもInkscapeでも読み込むことはできません。そのため、ちゃんとページサイズを指定しておきましょう。
Inkscape.png

HoudiniでPostscriptファイルを読み込む

上記ではIllustratorやInkscapeで読み込むにはページサイズを指定しておきましょうと言いましたが、Houdiniで編集する場合には都合が悪いです。実際にFile SOPを使ってそのepsファイルを読み込むとその線が非常に粗いことがわかります。
これはページサイズを指定したことで本来持っている精度が欠落してしまったからです。
例えば綺麗な画像の解像度を下げた後に解像度を上げるとめっちゃ粗くなるやつ。
Pagesize.png
そのため、wrenコマンドに-wと-hを指定しないでPostscriptを書き出します。
そして読み込んでみると・・・
NoPagesize.png
まずは、このワイヤーフレームをカメラから見て元のシーンと重なるようにしたいと思います。
方法は色々あると思いますが、もっともわかりやすい方法でやってみます。
File SOPの後に、EPSファイルの中心がXY原点になるようにトランスフォームさせ、それをカメラの画角から均一スケールをかければできます。接続は以下のとおり。
Transform.png
1個目のTransform SOPで
Translate Xのエクスプレッション:-0.5*ch("../../cam1/resx")*72
Translate Yのエクスプレッション:-0.5*ch("../../cam1/resy")*72
2個目のTransform SOPで
Uniform Scaleのエクスプレッション:-ch("tz")/ch("../../cam1/focal") * ch("../../cam1/aperture")/ (72*ch("../../cam1/resx"))
それをCamノードの子にする。
CameraTransform.png
これで、Postscriptをカメラの前に配置できるようになりました。
このワイヤーフレームの位置は、上記の2個目のTransform SOPのTranslate Zの値で調整することができます。
このZ値はカメラのNearクリップ平面よりは奥に位置するようにします。
CamProject.png
カメラ視点でみると、こんな感じになります。
CameraLook.png

HoudiniでポリゴンカーブをNURBSカーブに変換する

Houdiniの良いところは、ポリゴン/Bezier/NURBSの双方向の変換やOpenVDBとポリゴンの双方向の変換が得意なところかと思います。
Wrenで生成したカーブはポリゴンカーブなのでハンドリングしにくい上にカックカクです。
そのため、Houdiniを使ってポリゴンカーブをNURBSカーブに変換してみます。
まず、クリーンアップしていきます。
重複ポイントはFuse SOPで結合します。
プリミティブの数が多いのでPolyPath SOPを使って見た目が一本線に見えるプリミティブを結合しちゃいます。
以下のようにワイヤーフレームがすっきりしました。
polypath.png
シェーダボールに何か微小な線があるので、それはDelete SOPでエクスプレッションで自動的に消去します。
prim(ノードのパス,$PR,"intrinsic:measuredperimeter",0)<長さ
線の長さはintrinsic:measuredperimeterで調べることができます。
Sort SOPでポイント番号の並びをカーブ沿いに揃えたいのでBy Vertex Orderを実行します。
CleanUp.png
ポリゴンカーブをNURBSに変換する上で、以下のような一本線は都合が悪いです。
TroublesomeCurve.png
角張った一本線は分割したいです。線の分割はPolyCut SOPでできますが、パラメータを見てみると分割点を指定する必要がありそうです。んー手動でやりたくないんだよね:triumph:
PolycutParam.png
一本線が折れている部分の点をここに自動的に指定できるようにしたいです。そのために、折れた角度が特定の角度よりも大きい箇所の点を調べる必要があります。
なので、ラングっちゃいましょうか。線間の角度をPointアトリビュートとして収めるPrimitive Wrangleを作ります。
PrimWrangleSplit.png

PrimitiveWrangle
int listPoints[] = primpoints(0, @primnum);
for(int i=0;i<len(listPoints);i++){
    if( i==0 || i==len(listPoints)-1){
        setpointattrib(0, "primsplit", listPoints[i] , 0.0, "set");
    }
    else{
        vector curP = point(0, "P", listPoints[i]);
        vector preP = point(0, "P", listPoints[i-1]);
        vector postP = point(0, "P", listPoints[i+1]);
        float thresholdAngle = degrees( acos(dot(normalize(curP - preP) , normalize(postP - curP)) ) );
        setpointattrib(0, "primsplit", listPoints[i] , thresholdAngle, "set");
    }
}

ポイントに線間の角度が入ったprimsplitという名前の独自アトリビュートが作成されました。
primsplit.png
このアトリビュートをPolyCut SOPに指定することで、角張った一本線が分割されました。
primsplitResult.png
後は、For Eachブロックで各プリミティブ毎にConvert SOPでNURBSカーブに変換して、その後にUV Texture SOPでuv情報を付けます。
Convert.png
後は、ワイヤーフレームがカメラの前に表示されるように上記で説明した2個のTransform SOPに接続して、
あとは必要に応じて線幅を定義するためにwidthアトリビュートをuvアトリビュートに基づいて定義すればOKです。
width.png
NURBSカーブのポイント(制御点)に基づいてランプで線幅を変化させているので、真っ直線な線は一定で曲線には線幅が変わるようにしています。これがポリゴンカーブだと制御が難しいですよね。
CameraLookWire.png

一度仕組みを作成してしまえば、オブジェクトを入れ替えるだけで自動的にラインが生成されます。
トポロジーに依存していないので、アニメーションキャラクタだろうが、ボリュームから変換したポリゴンモデルだろうが追従します。
あとは、モデルサイズに応じてライン幅を調整したりすればいいです。

アニメーションキャラクタに適用した例:
Chara.gif

また同じ事を言いますが、Houdiniの良いところは、ポリゴン/Bezier/NURBSの双方向の変換やOpenVDBとポリゴンの双方向の変換が得意なところです。
Pyroで爆発を作成して、そのラインを作成したいのであれば、PyroのdenistyボリュームをOpenVDBに変換して、それをポリゴンに変換し、そのモデルに対してラインを作成することができます。
VDBConvert.png

ボリュームから変換したポリゴンモデルに適用した例:
塗り
Fill.gif
ライン
Wire.gif
これをコンプ
Comp.gif
ラインはポリゴンカーブではなくNURBSカーブなので柔軟に微調整することができます。

ここまでがWren ROPの使い方の説明になります。
次は、このNURBSカーブをドローソフトで使用できるようにSVGに変換したいと思います。
変換は手動じゃなくて自動でやりたいのでSOHOを使ってみます。

SOHOとは

MantraなりWrenなり触っているとsoho_なんちゃらのようなパラメータを見かけることがあります。
sohoparams.png
SOHOとは、Scripted Output of Houdini Objectsの略で、Houdiniオブジェクトの出力をスクリプト制御するものであることが予想できます。
SidefxのSOHOのヘルプを読んでみると、SOHOはHoudiniとレンダラーのPythonバインディングと書いてあります。バインディングとは分かりやすく日本語で説明すれば橋渡しのことで、要するにHoudiniはSOHOを介してMantraなどのレンダラーと通信します。
Mantra ROPやWren ROPは、デジタルアセットをUIとして、SOHOによってレンダリングが行なわれています。
では、Mantra ROPでsoho_なんちゃらのパラメータを見てみたいと思います。
soho_programパラメータが通常では非表示になっているのでそれを表示してみると、Mantra ROPではsoho_programパラメータにIFD.pyが指定されていることが分かります。
sohoProgram.png
Houdiniは、MantraでレンダリングまたはIFD出力をする時には、SOHOプログラムに対してIFD.pyを実行することが分かります。
Houdiniインストールディレクトリ\houdini\soho\python2.7には、Mantra用のIFD.pyやWren用のwren.pyなどのプログラムがあります。Houdiniインストールディレクトリ\houdini\soho直下にはSOHOに使用できるメソッドのヘルプが説明されているsoho.docがあります。

Houdiniとレンダラーを橋渡しする役割を担っているのがSOHOであり、独自でSOHOを利用したROPを作成することができます。
SOHOで用意されているメソッドは主にシーン内のどのオブジェクトをレンダリングするのかどうかやカメラの情報を取得といったレンダラー向けのメソッドがほとんどですが、別にレンダラー以外の他のアプリケーションとHoudiniを橋渡しするためにSOHOを使うこともできます
SOHOを利用した独自のROPはPython ROPで作成します。
Python ROPによるSOHOの使い方は、
SOHOのチュートリアル
が参考になると思いますが、ここでもそのSOHOの使い方を説明したいと思います。
ちなみに、Python ROPの実行はHoudini Apprentice/Indieのライセンスではできないようになっています。
これができてしまったらIFD.pyを使ってApprenticeでもIndieでもIFDの書き出しができちゃいますからね。仕方ない。

Houdiniと外部アプリケーションを橋渡しする方法には、

  • ROPに用意されているPre/Post Scriptパラメータを利用する
  • Shell ROPを利用する
  • Python ROPを利用する

がありますが、今回は柔軟性の高いPython ROPを利用してみます。

Python ROPを作成する

何かのPython ROPを作成してみたいと思います。
ここではNURBSカーブをSVGファイルに変換するパイプラインを構築したいと思います。
NURBSカーブをSVGに変換する機能はHoudiniには用意されていません。
そのため、他のアプリケーションでその処理をさせてみます。
FreeCADという無料のCADソフトではNURBSカーブが定義されたigesというファイルフォーマットを読み込んでSVGにエクスポートすることができます。HoudiniはNURBSカーブをigesファイルにエクスポートすることができます。このFreeCADというソフトは3DCADでよく使用されるstepファイルフォーマットのインポート/エクスポートにも対応している素晴らしいソフトで、しかもPythonから外部制御することができます。
つまり、HoudiniとFreeCADをSOHOを介してパイプに組み込むことができます!
まずはFreeCADをインストールしておきます。

Python ROPを作成するには、FileメニューからNew Assetを選択します。
表示されるNew AssetダイアログでOperator Name(内部ノード名)とOperator Label(ラベル)を好きなように入力し、Operator StyleOutput Drive Typeに、Save To LibraryにHDAファイルの保存先を指定します。
PythonROP2.png
Acceptを押すとEdit Operator Type Propertiesが表示されます。
ここでSOHOと通信するために必要なパラメータを作成します。
SOHOに最低でも必要なパラメータはsoho_programsoho_outputmodeです。
今回は以下のように4つのSOHOパラメータを追加しました。
PythonROP3.png
あとは、Python ROPをディペンデンシーに組むことができるように入力コネクタを持たせたいのでMaximum Inputsを大きい値にしておきます。デフォルトでは、このMaximum Inputsが0になっています。それだと入力コネクタを持たないROPが作成されてしまうので注意してください。
ディペンデンシーって何?:baby:って人のために説明しておくと、日本語で"依存関係"のことです。
例えば、シミュレーションをするROP、ジオメトリを生成するROP、レンダリングするROPがあったら、それらを処理の順番で数珠繋ぎしたものがディペンデンシーです。ROPを接続するだけで、最後に接続されたROPを実行するだけで、一連のROPの処理が行なわれる便利な機能です。
もし、ディペンデンシーがなかったら、
パソコンの前でタスクAが終わるのを待って、終わったら手動でタスクBの準備をして実行し、またその処理を待って、それが終わったらタスクCを手動で準備して実行するという非常に生産性の悪いことをしなければなりません。
ディペンデンシーを組めば、ボタンポチってすれば、すべてのタスクが自動化されるので、パソコンの前で待ち構えることせずに昼寝できるわけです。:sleeping:

sohoparamsBasic.png
今回はSOHOの使い方を学ぶのでそのままAcceptでHDAを作成します。
これでネットワークエディタのoutコンテキスト内でTabキー→Digital Assetsから作成したHDAを読み込むことができるようになりましたので配置します。
PutPythonROP.png
以下のようなGUIが表示されました。
PythonROPGUI.png

sohoパラメータの説明

追加した4つのパラメータが何のためにあるのかを説明します。

パラメータ名 パラメータタイプ 目的
soho_program String SOHOが実行するPythonファイル名(必須)
soho_outputmode Integer soho_programの結果の出力方法(必須) 0,1,2の数値で指定
soho_pipecmd String soho_programの結果を入力にするパイプコマンド(soho_outputmodeが0の時に必須)
soho_diskfile String soho_programの結果の出力ファイル(soho_outputmodeが1の時に必須)

soho_pipecmdに指定するパイプコマンドとは一体なんでしょうか?
通常のexeコマンド(例えば、houdinifx.exeとかPhotoshop.exeとか)と何が違うのでしょうか?
パイプコマンドは、常に何かしらの入力待ちで待機し続けるコマンドのことです。
mantra.exe、wren.exeはパイプコマンドです。Houdiniシェル上でmantraを実行してみれば分かると思いますが、mantraを実行してもコマンドは終了せず、ずっと入力待ちの状態になり、そこではIFDの命令コマンドを入力して実行すると、また命令コマンドが入力されるまで待機します。
もっと身近なケースでいうならftpコマンドやtelnetコマンドがそうです。
RPCでプロセス間通信する時にプロンプトが出ますが、あれです。
パイプコマンドのftpコマンドの例:
ftp.png
Mantra ROPのsoho_outputmodeパラメータはトグルボタンで制御されているので、そのトグルをオフ(0)/オン(1)に切り替えることでsoho_programに指定されているIFD.pyの結果をmantraパイプコマンドに渡すのか、IFDファイルに書き出すのか切り替えることができます。
Mantra ROPの例:
Mantra ROPのsoho_outputmodeが0の時は、soho_pipecmdで指定されているパイプコマンドが実行されるので、レンダーコマンドが次々に実行されていきます。
Mantra ROPのsoho_outputmodeが1の時は、soho_diskfileに指定されている場所にIFDファイルが生成されます。
MantraOutputMode.png

以上のことから、

  • soho_programで指定したPythonの実行結果をパイプコマンドに渡す時はsoho_outputmodeを0に設定します。
  • soho_programで指定したPythonの実行結果をディスクファイルに書き出す時はsoho_outputmodeを1に設定します。
  • soho_programで指定したPythonを従来通りに実行するだけの時はsoho_outputmodeを2に設定します。

soho用Pythonスクリプトの作成

では、SOHOに実行させるPythonプログラムを作成します。
カスタムのSOHO用Pythonプログラムは、基本的にはホームディレクトリ\houdiniバージョン\sohoの直下に配置します。sohoフォルダがなければ手動で作成します。このディレクトリは、soho_programで指定したPythonファイルが探される場所になっています。

soho用Pythonスクリプトの基本書式
import soho

#レンダラーとの通信ではinitializeでカメラを指定して初期化する必要があります。
#この記事ではレンダラー以外のプログラムと接続をするので基本的にはinitialize(0)でOK。
#initialize(0)を実行することで、printの結果はPythonコンソールではなく標準入力に出力されるようになります。
soho.initialize(0)
floatValue = soho.getDefaultedFloat("floatパラメータ名",[0.0])[0]#HDA上のfloatパラメータ値を参照
intValue = soho.getDefaultedInt("integerパラメータ名",[0])[0]#HDA上のintegerパラメータ値を参照
stringValue = soho.getDefaultedString("stringパラメータ名", [""])[0]#HDA上のstringパラメータ値を参照

print "書き出す文字列。soho_outputmodeが0ならパイプコマンドに渡す1行目のコマンド文字列"
print "書き出す文字列。soho_outputmodeが0ならパイプコマンドに渡す2行目のコマンド文字列"
print "書き出す文字列。soho_outputmodeが0ならパイプコマンドに渡す3行目のコマンド文字列"

です。sohoモジュールで使用できるメソッドは上記で説明しているsoho.docを参照するか、IFD.pyを参考にしてください。
soho.getDefaultedなんちゃらメソッドは、HDA上に追加したスペアパラメータの値を取得することができます。
1番目の引数にはスペアパラメータの内部名
2番目の引数にはデフォルト値
を指定します。2番目の引数がリストになっているのが変な感じがするかもしれません。
そもそも、スペアパラメータはサイズ1以上のコンポーネント(例えば、X,Y,Zコンポーネント)を定義することができるのでそのようになっています。
上記の例では、サイズ1のパラメータの値を変数に格納したいので、[0]で1番目のコンポーネント値を取得しています。

では、HoudiniからエクスポートしたigesファイルのパスをFreeCADに渡し、それをFreeCADでSVGにエクスポートするスクリプトを作成したいと思います。
FreeCADのインストールディレクトリ直下のbinフォルダにはFreeCADCmd.exeというコマンドがあります。
FreeCADCmd.png
これを実行してみると、Pythonコマンドを入力することができるパイプコマンドであることがわかります。
FreeCADPipeCMD.png
そのため、soho_outputmodeを0、soho_pipecmdにFreeCADCmd.exeを指定し、
soho_programに指定するPythonスクリプトは、そのパイプコマンドに渡すコマンド群を出力すればよいので

FreeCAD.py
import soho
import os
soho.initialize(0)
stringValue = soho.getDefaultedString("filepath", [""])[0]#filepathスペアパラメータの値を取得
if stringValue.endswith(".igs") and os.path.exists(stringValue):
    print "import Part"
    print "import importSVG"
    print "igesPath =" + "\""+stringValue+"\""
    print "svgPath = igesPath.rsplit(\".\",1)[0]+\".svg\""
    print "newDoc = FreeCAD.newDocument(\"new\")"
    print "Part.insert(igesPath,\"new\")"
    print "partObjects = newDoc.findObjects()"
    print "if len(partObjects)==0:"#Igesが空っぽの時はダミーの点を追加
    print "    partObjects.append(newDoc.addObject(\"Part::Vertex\"))"
    print ""#インデントから抜ける時に必要になります。
    print "importSVG.export( partObjects ,svgPath)"
    print "FreeCAD.closeDocument(\"new\")"

と記述することができます。
printステートメントが多いのが気になる方は、

FreeCAD.py
import soho
import os
soho.initialize(0)
stringValue = soho.getDefaultedString("filepath", [""])[0]#filepathスペアパラメータの値を取得
if stringValue.endswith(".igs") and os.path.exists(stringValue):
    print "import Part"
    print "import importSVG"
    print "igesPath =" + "\""+stringValue+"\""
    print """
svgPath = igesPath.rsplit(".",1)[0]+".svg"
newDoc = FreeCAD.newDocument("new")
Part.insert(igesPath,"new")
partObjects = newDoc.findObjects()
if len(partObjects)==0:
    partObjects.append(newDoc.addObject("Part::Vertex"))

importSVG.export( partObjects ,svgPath)
FreeCAD.closeDocument("new")
"""

でもいいです。ボクは前者の方が好きです。

処理内容は、指定したigesをFreeCADに読み込みigesと同じ場所にSVGをエクスポートします。
これをホームディレクトリ\houdiniバージョン\sohoの直下に例えばFreeCAD.pyという名前で保存します。

Sample ROPに"filepath"という名前の文字列スペアパラメータを追加し、SOHO ProgramにFreeCAD.py
SOHO Output Modeを0、CommandをFreeCADCmd.exeを指定します。
SampleROPsetting.png
こんな感じでディペンデンシーを組みます
Dependency.png
3Dシーンから抽出したワイヤーフレームを読み込んだFileオブジェクトをWren ROPで再度レンダリングするとループに入ってしまってHoudiniが落ちてしまうので、そのFileオブジェクトはWren ROPでレンダリングされないようにExclude Objectsに指定しておきます。
dep1.png
Geometry ROPは、SOP PathにNURBSカーブに変換したSOPを、Out Fileにigesの出力先を指定します。
dep2.png
Sample ROPは、Iges FileにGeometry ROPのOut Fileのパラメータを参照するようにします。
dep3.png

ディペンデンシーで一番最後に接続されたSample ROPをレンダリングすることで、Wren ROPとGeometry ROPが連動して処理されていくので、手作業を介さずに、Wrenから生成したPostscriptをIgesに変換してSVGにエクスポートするというワークフローを自動化することができました。

出力されたSVGをドローソフトで開いてみると、アンカーを自由にいじることができて滑らかな曲線が生成されていることがわかります。
Output.png

おまけ1:HoudiniからMayaへのバインディング

今回はSOHOを使ってHoudiniからFreeCADへのバインディングをしましたが、Mayaを起動した状態でMaya側でmaya.cmds.commandPort(name=":ポート番号", sourceType="python")を実行しておけば、HoudiniからMayaへのバインディングも可能です。MELコマンドをMayaに送信する場合は、maya.cmds.commandPort(name=":ポート番号", sourceType="mel")を指定します。

これで、DOSコマンドプロンプトでtelnetコマンド(標準ではインストールされていません)を実行し、
open localhost 8888コマンドでMayaに接続し、Ctrl+]キーでコマンド入力モードに切り替え
sendコマンドでPythonコードを送信することができます。
telnet.png
このtelnetコマンドをSOHOのパイプコマンドに指定したいのですが、その場合、telnetコマンドでMayaに接続した後にすぐにコマンド入力モードで入力待ち状態にする必要があります。
手動操作ならCtrl+]キーを押せばできるけどtelnetのオプションを見る限りはopenで接続した後でもコマンド入力モードのままにする方法が用意されていない:scream:
仕方ないので、Pythonスクリプトでtelnetlibモジュールを利用します。

HtoMconnect.py
# -*- coding:utf-8 -*-
#通常のPython.exeで日本語が記述されたスクリプトを実行するには、このcoding表記が必要
import sys
import telnetlib

host = sys.argv[1]
port = sys.argv[2]
terminal = telnetlib.Telnet(host,port)
inputString = raw_input(">")
while "quit" != inputString :#quitの命令を受け取ると終了する
    terminal.read_until(">",0.5)#ネットワーク環境によっては、1秒などの遅延に変更する
    terminal.write(inputString)
    inputString = raw_input(">")

あとは、telnet接続して、コマンドをMayaに送ればMayaを遠隔操作することができます。
pythontelnet.png

sendCommand.py
import soho
soho.initialize(0)

fps = soho.getDefaultedInt("state:fps", [0.0])[0]
time = soho.getDefaultedFloat("state:time", [0.0])[0]
frame = fps*time + 1

print "maya.cmds.sphere()"#Mayaのコマンドを書き出す
print "maya.cmds.move(" + str(frame) + ", 0.0, 0.0 )"#Mayaのコマンドを書き出す
print "quit"#最後はquitで待ち入力ループから抜ける。

soho_pipecmdpython Scriptのパス/HtoMconnect.py localhost ポート番号
を指定(.pyファイルの場所を指定すること)してあげて、soho_programにMaya Pythonコマンドを書き出すPythonスクリプトを指定(sohoフォルダにあればファイル名のみでOK。)してあげればHoudiniからMayaへのワークフローも自動化することができます。
例:
sohoMaya.png

ここで紹介したスクリプトは、SOHOを使わない単独で動くコードに修正して、何かしらのROPで用意されているPre/PostなんたらScriptパラメータに指定するという手もありますが、ディペンデンシーへの組み込みの柔軟性を考えれば、SOHOをレンダラーのバインディング用途以外にも使えるのではないでしょうか?
SOHOは便利っすよ:relaxed:

おまけ2:MayaからHoudiniへのバインディング

HoudiniからMayaへのバインディングを説明しましたが、逆にMayaからHoudiniへのバインディングも説明しておきます。

やっぱ、SOHOについて説明したんだし、HoudiniとMayaのバインディングも双方に説明しとく必要あるかなと思って。:ghost:

HoudiniにもMayaでいうcommandPortがあります。
SideFXのRPCのヘルプを読めば、HoudiniでもRPC(リモートプロシージャルコントロール)ができることがわかります。

Houdini側でPythonシェルから以下のコードを実行するか、または、123.pyに登録します。

RPCサーバー
import hrpyc
hrpyc.start_server(port=ポート番号)#portを指定しなかった場合は18811番が使われます。

このhrpycモジュールは、RPyC PythonモジュールをHoudini用にカスタマイズされたものです。つまり、Houdini以外のプログラムからHoudiniに接続する時は、このRPyCモジュールを使用すれば接続することができます。

Maya側にRPyC Pythonモジュールをインストールします。
これをダウンロードするよりも、Houdniインストールディレクトリ\python27\lib\site-packages直下にあるrpycフォルダをホームディレクトリ\maya\scripts下にコピーすればMaya側のPython上でRPyCモジュールを使用できるようになります。

MayaのScript Editor上で以下のPythonスクリプトを実行すれば、Houdiniにその命令を送ることができます。

Maya側で実行するスクリプトの例
import rpyc
connection = rpyc.classic.connect("localhost", 8888)
hou = connection.modules["hou"]#import houと同じ挙動になる。これでhouと同じPythonコードをそのまま記述できる
geo = hou.node("/obj").createNode("geo")
for i in geo.children():
    i.destroy()
sphere = geo.createNode("sphere")
grid = geo.createNode("grid")
grid.parm("rows").set(5)
grid.parm("cols").set(5)
copy = geo.createNode("copytopoints")
copy.setInput(0,sphere)
copy.setInput(1,grid)
copy.setDisplayFlag(True)
copy.setRenderFlag(True)
geo.layoutChildren()

これによって、Mayaを立ち上げた状態でHoudiniのPythonスクリプトを開発することができます。
Houdiniなんて嫌いだ、Mayaを使え!お前ら!って奴隷のように働かされてる隠れHoudinistも堂々とMaya上でスクリプトが書けるようになります。

おしまい:hugging: