今回はHoudiniのお話です。前回の記事でWrangleの疑問点をまとめました。
Twitterにて広くアドバイスを募ったところ、Satoru Yonekura(@yone80)さんにアドバイスを頂き疑問が解消しました!
情報が多くなりそうなので、記事は2つに分けることとしました。まずは本記事に理解したところをまとめ、その知見を元に前回の回答編として記事を書こうと思います。
Wrangleそのものを理解する前に、コンポーネント(Primitive, Point, Vertex, Edge, Detail)の復習が不可欠であったので主にそこに焦点を当てた記事になります。
- コンポーネントと参照
- Wrangleオペレータ(Run Overと各エレメントへのアクセス)
- 頂いたアドバイスの引用
- まとめ
コンポーネントと参照
前回の疑問点においてEdgeの項目がなかったので今回付言はしませんが、基本的に各エレメントの参照構造が分かれば理解できるかと思います。
また、コンポーネントとアトリビュートについての考察は、以前の記事にまとめてあります。
上記画像にPoints/Vertices/Primitivesを表したGIFアニメを貼りました。ビューポートとGeometry Spreadsheetを見ながら各構造をまとめていきます。
Points
ポイントはHoudiniで最も基本となるエレメントです。特に難しい点はないかと思います。
Vertices
Vertexはプリミティブ番号:プリミティブ内の頂点番号
と定義され、最終的に一意のPointを参照します。
上記例を見ると、ポイント番号1を参照している頂点はふたつあります。
- プリミティブ0:プリミティブ内の頂点番号1
- プリミティブ1:プリミティブ内の頂点番号0
ということになります。
ここでいうプリミティブ内の頂点番号は通常表示されないので分かりにくいですが、ビューポート上でDキー
を押すと表示されるDisplay Options > Markers > VerticesのNumbersにチェックを入れると表示されます。
Primitives
プリミティブも分かりやすいですね。本例ではプリミティブは2つです。プリミティブはバーテックスを参照します。
Wrangleオペレータ(Run Overと各エレメントへのアクセス)
ここが僕の理解の足りなかったところで、理解が深まったことで想定通りの挙動が確認できました。
まずは先に理解した内容をまとめておきましょう。
Run Over
VEXの対象となるエレメントを決めるオプション。エレメントの個数分だけ並列処理が実行されます。そして、そのエレメントを通して共有しているコンポーネントのアトリビュートを参照します。
ここがよく分かっていなかったためRun Overの対象と取得したいコンポーネントのアトリビュートが異なっている場合の挙動が把握できなかったということになります。
またPointを共有しているプリミティブが複数ある場合、@primnum
の値は最初に見つかったひとつが返ってくるというのも理解不足だった点です。
以下にサンプルファイルと合わせて挙動を確認していきましょう。
サンプルファイル
Run Over:Points/参照アトリビュート:@primnum
Run Overと参照アトリビュートを固定して様々な形状のジオメトリに対して検証していきます。
pointwrangle1
ポイント番号1,2がプリミティブを共有しています。
printf(sprintf('%g', @primnum) + '\n'); //ポイント総数は4 /* 0 // @ptnum == 0のケース、これは確実に0 1 // @ptnum == 1のケース、primitive0と1、どちらか先にアクセスできた方の@primnumが返ってくる 1 // @ptnum == 2のケース、primitive0と1、どちらか先にアクセスできた方の@primnumが返ってくる 1 // @ptnum == 3のケース、これは確実に1 */
pointwrangle2
形状を変化させました。ポイント番号1がプリミティブを共有しています。
printf(sprintf('%g', @primnum) + '\n'); //ポイント総数は5 /* 0 // @ptnum == 0のケース、これは確実に0 1 // @ptnum == 1のケース、primitive0と1、どちらか先にアクセスできた方の@primnumが返ってくる 1 // @ptnum == 2のケース、これは確実に1 0 // @ptnum == 3のケース、これは確実に0 1 // @ptnum == 4のケース、これは確実に1 */
pointwrangle3
形状は先程と同じですが、Blast SOP下流にFuse SOPを接続し、ポイントの共有を外しています。
ポイントの共有がなくなったためすべての@primnum
が一意に決定されます。
printf(sprintf('%g', @primnum) + '\n'); //ポイント総数は6 /* 0 // @ptnum == 0のケース、これは確実に0 0 // @ptnum == 1のケース、これは確実に0 1 // @ptnum == 2のケース、これは確実に1 0 // @ptnum == 3のケース、これは確実に0 1 // @ptnum == 4のケース、これは確実に1 1 // @ptnum == 5のケース、これは確実に1 */
pointwrangle4
今度はGrid SOPで確認していきます。ポイント番号1,3,4,5,7がプリミティブを共有しています。
printf(sprintf('%g', @primnum) + '\n'); //ポイント総数は9 /* 0 // @ptnum == 0のケース、これは確実に0 0 // @ptnum == 1のケース、primitive0と1、どちらか先にアクセスできた方の@primnumが返ってくる 1 // @ptnum == 2のケース、これは確実に1 0 // @ptnum == 3のケース、primitive0と2、どちらか先にアクセスできた方の@primnumが返ってくる 0 // @ptnum == 4のケース、primitive0,1,2,3、先にアクセスできた@primnumが返ってくる 1 // @ptnum == 5のケース、primitive1と3、どちらか先にアクセスできた方の@primnumが返ってくる 2 // @ptnum == 6のケース、これは確実に2 2 // @ptnum == 7のケース、primitive2と3、どちらか先にアクセスできた方の@primnumが返ってくる 3 // @ptnum == 8のケース、これは確実に3 */
pointwrangle5
形状は先程と同じですが、Attribute Wrangle SOP下流にFuse SOPを接続し、ポイントの共有を外しています。
ポイントの共有がなくなったためすべての@primnum
が一意に決定されます。
printf(sprintf('%g', @primnum) + '\n'); //ポイント総数は15(fuseでポイントの共有を外してるのですべてのprimnumは確定される) /* 0 // @ptnum == 0のケース、これは確実に0 0 // @ptnum == 1のケース、これは確実に0 1 // @ptnum == 2のケース、これは確実に1 0 // @ptnum == 3のケース、これは確実に0 0 // @ptnum == 4のケース、これは確実に0 1 // @ptnum == 5のケース、これは確実に1 2 // @ptnum == 6のケース、これは確実に2 2 // @ptnum == 7のケース、これは確実に2 3 // @ptnum == 8のケース、これは確実に3 3 // @ptnum == 9のケース、これは確実に3 3 // @ptnum == 10のケース、これは確実に3 1 // @ptnum == 11のケース、これは確実に1 2 // @ptnum == 12のケース、これは確実に2 3 // @ptnum == 13のケース、これは確実に3 2 // @ptnum == 14のケース、これは確実に2 1 // @ptnum == 15のケース、これは確実に1 */
pointwrangle6
Fuse SOPでポイントの共有を解除したあと、各々のプリミティブを縮小しています。
printf(sprintf('%g', @primnum) + '\n'); //ポイント総数は15(見やすくするため各々の面を小さくした) //pointwrangle5同様すべてのprimnumは確定される /* 0 // @ptnum == 0のケース、これは確実に0 0 // @ptnum == 1のケース、これは確実に0 0 // @ptnum == 2のケース、これは確実に0 0 // @ptnum == 3のケース、これは確実に0 1 // @ptnum == 4のケース、これは確実に1 1 // @ptnum == 5のケース、これは確実に1 1 // @ptnum == 6のケース、これは確実に1 1 // @ptnum == 7のケース、これは確実に1 2 // @ptnum == 8のケース、これは確実に2 2 // @ptnum == 9のケース、これは確実に2 2 // @ptnum == 10のケース、これは確実に2 2 // @ptnum == 11のケース、これは確実に2 3 // @ptnum == 12のケース、これは確実に3 3 // @ptnum == 13のケース、これは確実に3 3 // @ptnum == 14のケース、これは確実に3 3 // @ptnum == 15のケース、これは確実に3 */
Run Over:Primitives/参照アトリビュート:@ptnum
今度はRun OverをPrimitivesにし、各々のプリミティブを通して@ptnum
を参照してみましょう。
プリミティブはバーテックスを参照し、バーテックスはポイントを参照します。ポリゴンが定義されるには最低3つのバーテックスが必要なので、ひとつのプリミティブからアクセスされるのは3つ以上のポイントが対象となります。
primitivewrangle1
printf(sprintf('%g', @ptnum) + '\n'); //プリミティブ総数は2 /* 0 // @primnum == 0のケース、point0,1,2、先にアクセスできた@ptnumが返ってくる 1 // @primnum == 1のケース、point1,3,2、先にアクセスできた@ptnumが返ってくる */
primitivewrangle2
printf(sprintf('%g', @ptnum) + '\n'); //プリミティブ総数は2 /* 0 // @primnum == 0のケース、point0,1,3、先にアクセスできた@ptnumが返ってくる 1 // @primnum == 1のケース、point1,2,4、先にアクセスできた@ptnumが返ってくる */
primitivewrangle3
printf(sprintf('%g', @ptnum) + '\n'); //プリミティブ総数は2 /* 0 // @primnum == 0のケース、point0,1,3、先にアクセスできた@ptnumが返ってくる 5 // @primnum == 1のケース、point5,2,4、先にアクセスできた@ptnumが返ってくる */
primitivewrangle4
printf(sprintf('%g', @ptnum) + '\n'); //プリミティブ総数は4 /* 0 // @primnum == 0のケース、point0,1,3,4、先にアクセスできた@ptnumが返ってくる 1 // @primnum == 1のケース、point1,2,4,5、先にアクセスできた@ptnumが返ってくる 3 // @primnum == 2のケース、point3,4,6,7、先にアクセスできた@ptnumが返ってくる 4 // @primnum == 3のケース、point4,5,7,8、先にアクセスできた@ptnumが返ってくる */
primitivewrangle5
printf(sprintf('%g', @ptnum) + '\n'); //プリミティブ総数は4 /* 0 // @primnum == 0のケース、point0,1,4,3、先にアクセスできた@ptnumが返ってくる 15 // @primnum == 1のケース、point15,2,5,11、先にアクセスできた@ptnumが返ってくる 14 // @primnum == 2のケース、point14,12,7,6、先にアクセスできた@ptnumが返ってくる 13 // @primnum == 3のケース、point13,10,8,9、先にアクセスできた@ptnumが返ってくる */
primitivewrangle6
printf(sprintf('%g', @ptnum) + '\n'); //プリミティブ総数は4 /* 0 // @primnum == 0のケース、point0,1,3,2、先にアクセスできた@ptnumが返ってくる 7 // @primnum == 1のケース、point7,4,5,6、先にアクセスできた@ptnumが返ってくる 11 // @primnum == 2のケース、point11,10,9,8、先にアクセスできた@ptnumが返ってくる 15 // @primnum == 3のケース、point15,14,12,13、先にアクセスできた@ptnumが返ってくる */
Run Over:Detail/参照アトリビュート:@ptnum,@primnum
いままでの結果はすべて想定通りでしたが、これだけ不思議な挙動となりました。Detailコンポーネントはジオメトリ全体を指すのですべてのPoint、Primitiveを包含するのかなと思っていたのですが、参照は持たないということで-1
が返ってくるのかもしれません。
下記公式ドキュメントにもDetailはPoint listとPrimitive listを持ってるぽい記述があるのですが。
引き続き要研究です。
detailwrangle1
printf(sprintf('%g', @ptnum) + '\n'); //ディテール総数は常に1 /* -1 // ディテールはポイントへの参照を持っていないってことかな */ printf(sprintf('%g', @primnum) + '\n'); /* -1 // ディテールはプリミティブへの参照を持っていないってことかな */
頂いたアドバイスの引用
ぼくの認識違いがあるかもしれませんので、YonekuraさんにTwitterで頂いたアドバイスを引用として記載しておきます。
例の場合1つのポイントが3つのプリミティブから共有されています、ポイントに対して返ってくるプリミティブはその3つのうちどれかで順序は保証されません、おそらくpoints, vertices, primitivesの関係が理解できていないと思うのでDocで確認するとよいと思います
— Satoru Yonekura (@yone80) 2017年9月19日
printf()での疑問も一部は先のジオメトリの構造が理解できると解決すると思います、順序についてはVEXは並列で実行され処理が終わった順序で結果が返ってくるのでバラバラな順序になります
— Satoru Yonekura (@yone80) 2017年9月19日
printf()で結果が一度しか返ってこない例はドキュメントでの説明は見つけられなかったのですがジオメトリにアクセスするかどうかで判断されているように見えますね
— Satoru Yonekura (@yone80) 2017年9月19日
正確にはPrimitiveがVertexを参照して、VertexがPointを参照するという順番ですね
— Satoru Yonekura (@yone80) 2017年9月19日
あとprimnumは順次呼ばれるのではなく、そのポイントを参照しているプリミティブのうち最初に見つかった一つが返ってきます
まとめ
Detailコンポーネントに関しては釈然としない感じになりましたが、それ以外は納得の行く結果になりました。
Run Overで指定したエレメントを通して各種アトリビュートにアクセスするという考え方をおさえておけば良さそうです。
アドバイスいただいたYonekuraさんに重ねてお礼を申し上げて記事を終えたいと思います。