kick the base

Houdiniと、CG技術と、日々のこと。

Houdini: JavaScriptと比較してWrangle(VEX)を理解する

本ブログではHoudiniのWrangleやVEXについていくつかの考察記事を書いてきました。

Houdiniを使い続けていく中でより考えがまとまってきたので知識を整理するために記事を書いておきます。

本記事では学び初めの頃しっくり来なかったWrangleのRun Overの仕組みとVEXの並列処理について、個人的に馴染み深いスクリプト言語であるJavaScript*1と比較しながら振り返ってみます。

本記事は現時点での僕の理解をまとめたものですので、間違いが含まれている可能性があります。ご容赦ください。

サンプルファイル

sample scene

VEXpressionテキストエリアとRun Overの仕組み

TABキーで呼び出せるPoint Wrangle、Primitive Wrangle、Vertex Wrangleの中身はAttribute Wrangleで、Run Overのみが異なっています。よく使うものをテンプレートとして用意してくれているというわけですね。*2

下記画像をご覧ください。

f:id:kickbase:20180221121320j:plain

ここでは

  1. Run Overが指定するものは何か
  2. VEXpressionはJSで言うところのどこを表しているのか

に焦点を当てて考察していきます。

VEXpressionテキストエリア

Point Wrange シュミレーション

JSとVEXで似たような状況をシュミレーションしてみました。イメージとしては下記画像の状態をJSで再現したと思ってください。※VEXコードを実行した出力結果については後ほど見ていきます

f:id:kickbase:20180221121346j:plain

続いてJSのコードを見てみましょう。*3

上記実行結果は下記のとおりです。実行はコマンドラインで

$node point_wrangle_sample.js

とかでいいでしょう。

Vimmerであれば外部コマンドの実行、:!node %でももちろんOKですね

出力結果

point.ptnum: 0
point.numpt: 4
point.P: 0,0,1
point.P[0]: 0
point.Alpha: 1
point.pscale: 0
-----
point.ptnum: 1
point.numpt: 4
point.P: 0,0,-1
point.P[0]: 0
point.Alpha: 1
point.pscale: 0
-----
point.ptnum: 2
point.numpt: 4
point.P: 1,0,0
point.P[0]: 1
point.Alpha: 1
point.pscale: 0
-----
point.ptnum: 3
point.numpt: 4
point.P: -1,0,0
point.P[0]: -1
point.Alpha: 1
point.pscale: 0
-----

何も難しいことはありませんね。

  1. ポイントを模したp0からp3をオブジェクトとして定義
  2. それらのポイントを配列pointsに格納
  3. for文で配列を走査します。配列のメンバをそれぞれローカル変数pointに代入、メンバのパラメータにアクセスし出力します

ここで重要なのはforループ内のpoint.hogeがVEXでいうところの@hogeに相当すること。

またAttribute WrangleのVEXpressionに記述されるコードはfor文内のコードブロックに相当することです。

VEXの@は他の言語でいうところのフェッチに相当しますが、「ああ、オブジェクトのパラメータにアクセスしてんのね」という感覚的な理解が大事なのかなと思います。

続いてPrimitive Wrangleを見ていきましょう。

Primitive Wrange シュミレーション

イメージ的には下記の通り。VEXコードもそうですが、Geometry Spreadsheetも参考ください。これをJSで書いてみます。

f:id:kickbase:20180221121412j:plain

これも実行してみましょう。

出力結果

prim.primnum: 0
prim.numprim: 2
prim.shop_materialpath: "/mat/iron"
prim.Cd: 0.37,0.96,0.41
-----
prim.primnum: 1
prim.numprim: 2
prim.shop_materialpath: "/mat/plastic"
prim.Cd: 0.7,0.04,0.93
---

こちらも難しくないでしょう。

  1. プリミティブを模したprim0prim1をオブジェクトとして定義
  2. それらのプリミティブを配列primsに格納
  3. for文で配列を走査します。配列のメンバをそれぞれローカル変数primに代入、メンバのパラメータにアクセスし出力します

Pointのときと全く同じです。

  • forループ内のprim.hogeがVEXでいうところの@hogeに相当する
  • Attribute WrangleのVEXpressionに記述されるコードはfor文内のコードブロックに相当する

ということですね。

Run Overの仕組み

ここまで見れば一目瞭然ですね。Run OverはJSのfor文で言うところのpointsprimsを示します。そんなの分かっとるがな。という方も多いかと思いますが、ややこしいのが実際のジオメトリではpointやprimitiveには相関性があり、Run Overのターゲットとは異なるコンポーネント、そのアトリビュートにアクセスできてしまうという点です。

これはvertexがpointの参照として定義されていたり、またそれらの集合としてprimitiveが形成されているためなかば当然に起こることなのですが、ここが混乱を生みます。(ぼくは混乱しました)

現在の僕の理解、イメージ、作法としては、Run Overで指定したコンポーネント以外のアトリビュートにはアクセスしないという考え方に落ち着いていて、他のコンポーネントのアトリビュートを使いたい場合は、Attribute Promoteなどを使って事前にRun Overのターゲットに移植しておくという手法が最もスマートだと思っています。

WrangleのRun Overは君しか見えない!君以外考えられない!というイメージで使うと良いのかなと思う次第です。

VEXの並列処理とは

VEXという言語があるのにわざわざJSで解説したのは、もちろん僕がJSに慣れているからというだけではありません。他の言語と比較することで違いが分かりやすいかと思ったからです。

サンプルファイルのノードでどのような出力になるか見てみましょう。

pointwrangle1

f:id:kickbase:20180221121504j:plain

出力結果

@ptnum: 0
@ptnum: 1
@ptnum: 2
@ptnum: 3
@numpt: 4
@P: {0.0,0.0,1.0}
@P: {0.0,0.0,-1.0}
@P: {1.0,0.0,0.0}
@P: {-1.0,0.0,0.0}
@P.x: 0.0
@P.x: 0.0
@P.x: 1.0
@P.x: -1.0
@Alpha: 1.0
@pscale: 0.0
-----

primitivewrangle1

f:id:kickbase:20180221121520j:plain

出力結果

@primnum: 0
@primnum: 1
@numprim: 2
@shop_materialpath: "/mat/iron"
@shop_materialpath: "/mat/plastic"
@Cd {0.37,0.96,0.41}
@Cd {0.70,0.04,0.93}
-----

verticeswrangle1

f:id:kickbase:20180221121533j:plain

出力結果

@ptnum: 0
@ptnum: 1
@ptnum: 2
@ptnum: 0
@ptnum: 1
@ptnum: 3
@numpt: 4
@creaseweight: 3.0
@creaseweight: 3.0
@creaseweight: 3.0
@creaseweight: 0.0
@creaseweight: 3.0
@creaseweight: 0.0
-----

出力を確認しよう

見ての通り、JSとは出力のされ方が異なっていますね。

JSのようにコードブロックの上から下まで評価してから次のメンバに行くのではなく、1行のコードを各メンバごとに評価し、すべてのメンバが処理し終えたら次の行を評価しに行くという順番になっています。

そして@numpt@numprim、本例での@Alpha@pscaleなどすべてのターゲットで同じ値のものは1行だけ出力されています。*4

これがVEXの並列処理と呼ばれるものです。この処理順序を意識するシーンはそれほど多くないでしょうが、プリントデバッグを行うケースでは仕組みを理解していたほうが良いでしょう。

また、Attribute Wrangleは基本的に、Run Overで指定したコンポーネントのひとつひとつの要素を順に処理していくため、要素ひとつひとつの変更がドミノ倒し的(帰納的)に反映されるような実装はできません。そのような処理を行いたい場合はRun OverをDetail(Once Only)に指定する必要があります。並列処理が理解できるとここもスッキリするのではないでしょうか。

まとめ

いかがでしたでしょうか。今回まとめた内容はどれもぼくが去年つまづいた部分になります。

じっくりと考え直したきっかけは下記表現を作っていた時です。

これについてはアドバイスを頂き理解が深まったり、またCookの実行速度などを意識するいい機会になったので、次回の記事でまとめてみようと思います。

本記事がどなたかのお役に立てば幸いです。

*1:ビコーズ アイ アム ウェッブマン

*2:Deformation Wrangle、HeightField Wrangle、Volume Wrangleは別物なのでここでは割愛します

*3:僕はオシャレじゃないのでletとかconstとか使いません。悪しからず

*4:もちろんポイントごとに透明度や大きさが異なっている場合は複数出力されるでしょう