チュートリアルでつまづいたシリーズ：凹(谷折り、Concave)エッジのPolyExtrude

## 実行環境
Windows11 Houdini 20.5.445　Educational License

## 記事の内容と対象読者
こんにちは。私は元グラフィックス・エンジニアで、現在は大学教員をしています。Houdiniは研究室で扱っているだけで科目ではまだ扱っていないのですが、CGのアルゴリズムやデータ構造を説明するのに最適な教材はHoudiniだと考えているので、いろいろな教科書や動画記事を参考にしながらHoudiniを扱う科目のシラバスを思案しているところです。

そんなおり、Houdiniの前身であるPrismまで遡る無駄に長い私のプロシージャル道において、何度となくつまづいてきたポイントにまたしてもつまづきました。同じ教科書や動画記事で勉強している私と同じレベルの人のお役に立ったり、詳しい人にはアドバイスを頂けたら大変うれしいので記事にしていきます。よろしくお願いします。

## お題の教材

https://youtu.be/CQEZyeDNLZE?t=946

[Entagma](https://entagma.com/)さんの初心者向けKineFX/APEX動画３本シリーズの第1回です。英語ですがHIPファイルもありますし、KineFX/APEXの入門として素晴らしくわかりやすいのでおススメです。

で、期待していた人にはすみませんが、私がつまづいたのは本題のKineFX/APEXではなく、プロシージャルモデリングをAPEXネットワークでやってみようという箇所の作例に関してです。この動画はモデリングを意図しているわけではないのでまったく言及されていませんが、嫌な感じのトポロジーが出来ている箇所があってレンダリングがおかしくなってます。


ちょっと気になるので、１分もかからないつもりでそれを修正しはじめました。そして、これまで何度もつまづいたことにまたつまづいて結局１時間くらいかかりました。これは記事にして残しておかないとというわけです。

## 問題：凹エッジを持つジオメトリをポリゴンプリミティブ単位でバラバラに押し出すといろいろとっ散らかります。
動画では正12面体をポリゴンプリミティブ単位で２回押し出しています。PolyExtrudeSOP.DivideIntoパラメーターがIndividualElementsになっているのを覚えておいてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/9980d15b-f0e8-4427-b5b9-48203fc5edd0.png)

![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/f38deca6-fb50-44cd-89e0-2a91bd750b7e.gif)

一見問題なさそうですが、よく見ると凹エッジだった部分でPolyExtrudeSOPに押し出された形状同士のインターセクトやオーバーラップが起きています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/44b56c0e-2c0e-421d-bba1-03f5e0d6bf12.png)

こういう箇所はレンダリングの際に意図しないアーティファクトを生じます。図は問題個所をレンダリングしたものですが、オーバーラップが継ぎ目として表れていますし、インターセクションはジャギーな線として表れてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/e7208851-e459-4932-87cc-c5bc903cdfaf.png)

問題を単純化するとこういう事です。PolyExtrudeSOPは法線に沿って面を押し出すわけですが、IndividualElementsで複数の面を同時に押し出すと面同士の角度によってはお互いにぶつかってしまうわけです。

![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/fe2d96a7-e8b6-4c33-99ca-789fd905eb31.gif)

このような箇所は個別に抜き出して、PolyExtrudeSOP.DivideIntoパラメーターをConnectedComponentとしてから押し出す必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/a1f3910a-b33a-4b33-990b-9a0837938544.png)
![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/7ccb22c5-5632-419c-8874-fc8e1ff8592a.gif)

## 提案：問題解決手法
今回の作例の問題点が整理できましたので、このような手順で修正していきます。
1. ジオメトリの凹エッジを抽出してエッジグループvalleyを作る
1. valleyに所属する凹エッジを共有する２枚のポリゴンプリミティブを探し出す
1. その２枚にプリミティブアトリビュートpairを作成して同じ値にしておく
1. pairの値が同じ２枚のポリゴンプリミティブを抜き出す
1. その２枚だけを対象にConnectedComponentとして押し出す
1. 以下、４と５の繰り返し


### 手順1：GroupCreateSOP
今回の場合「エッジを共有する２枚のポリゴンプリミティブの角度が９０より小さい」ときに凹エッジとして判定してvalleyエッジグループに入れればよさそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/b85384a6-08f2-4d96-854f-38d0f40796a3.png)

### 手順2,3：WrangleSOP (RunOverDetail)
VEXでvalleyエッジグループからエッジを一つづつ抜き出してそのエッジを共有する２枚のポリゴンプリミティブに対してアトリビュートpairを作成するプログラムを書きます。アトリビュートはすべてのプリミティブに対してデフォルト値で作成されるので、スプレッドシートを見るとわかりますが、凸エッジしか持たないプリミティブのpairは空文字列になっています。あとでわかりますがこれはとても都合がよいです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/12bca273-b146-444e-9b05-315ecc5dd321.png)

VEXはこのようになります。エッジグループとは言っても両端点のポイント番号がセットで並んだ配列が帰ってくるだけです。VEXでエッジグループ単位の処理を書くのは、ほかのコンポーネントに比べると少し面倒なのです。
~~~c
//エッジグループvalleyを取得
int edge_group[] = expandedgegroup(0, "valley");

//エッジを一つづつ抜き出す
for (int i = 0; i<len(edge_group); i+=2 ){
    
    //エッジを共有するポリゴンプリミティブを探し出す
    int p0s[] = pointprims( 0, edge_group[i] );
    int p1s[] = pointprims( 0, edge_group[i+1] );
    int found[];
    
    foreach( int p0; p0s ){
        foreach( int p1; p1s ){
            if( p0 == p1 ) push( found, p0 );
        }
    }
    
    //ユニークなＩＤでアトリビュートpairを作成して
    //２枚のポリゴンに埋め込んでおく
    string name = itoa(found[0]) + "_" + itoa(found[1]);
    setprimattrib(0, "pair", found[0], name, "set" );
    setprimattrib(0, "pair", found[1], name, "set" );
} 
~~~
:::note info
エッジはHoudiniのファーストクラスコンポーネントではないので、ほかのコンポーネントと違いVEXの定型処理というのがなく、いくつかやり方があります。今回のようにRunOverDetailで処理をシリアライズするとVEXの並列性を活かせません。RunOverPrimitiveでハーフエッジ検索などした方が早いかもしれませんが、プログラム的に複雑になるので今回はこれにしました。そもそもSOPだけで同じことができるはずだとずっと思っていて試してはつまづいております。詳しい方は、是非、ご教授ください。
:::

ここで試しにExplodedViewSOPを使ってpairアトリビュート単位でプリミティブを分割してやるとこのようになります。意図した分割が出来ていることがわかります。この単位で２回目のPolyExtrudeSOPを個別に適用できればよいわけです。

![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/d2e4a536-b15b-47b6-a1ea-7670dd6cc8aa.gif)

### 手順4,5: For-EachNamedPrimitive
pairアトリビュートが同じ値のプリミティブを抜き出して、PolyExtrudeSOPを適用し、本体とマージする、を繰り返します。For-EachNamedPrimitiveを作成してSOPのForループを作ります。BlockEnd.PieceAttributeパラメーターをpairにしておくのが重要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/12bcf6b3-3153-4df2-92e7-38c4167a2ee5.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/4b6eaf19-76eb-430d-8e10-63d68d40273d.png)

BlockBeginSOPとBlockEndSOPの間に二つ目のPolyExtrudeSOPを挿入し、DivideIntoパラメーターをConnectedComponentにしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/0ad498e7-6773-4d8e-947d-d3c4e13b72b5.png)

Forループから出てきたジオメトリーはマージされていますが、先ほどと同様にExplodedViewSOPで見るとわかる通り、トポロジー的にいくつかの島に分かれています。

![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/e5f4f8a7-78f1-454a-a1c6-0273071a1896.gif)

これだと後々いろいろな問題が起きるのでFuseSOPを使ってひとつながりにしておきます。オンオフすると頂点が共有されることでシェーディングの様子が変わるのがわかります。

![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/3018cf04-a447-484d-99f1-7c11de46e68f.gif)

## 提案手法に関する考察
BevelSOP、NormalSOP、ColorSOPを追加して元の作例と提案手法の作例をSwitchSOPで比較してみます。最初が元の作例で次が提案手法の作例です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/385fa25f-bcf4-49ff-8b87-67ecf8c20ae0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/74ac8386-59e0-46e6-847e-f459b1d652bf.png)

一見よさそうなんですが、BevelSOPのパラメーターによっては出てきてしまう意図しないアーティファクトが気になります。レンダリングしたときにアンビエントオクルージョンがかかるところですが、ライトによってはやたらと明るく見えてしまう事があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/f12951e9-df8d-4a48-9595-e043fd9200fa.png)

画角に応じてBevelSOPの値をもっと小さくすればよいのですが、そもそも滑らかにする必要がないエッジにベベルがかかっているのが問題だと思います。平面に収まってるエッジにベベルかける必要ないですよね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/67c8e5f8-7dd5-4f91-b11f-7af9a8d4cc19.png)

そこで、GroupCreateSOPでエッジを共有するポリゴンプリミティブの角度がほとんどないエッジグループを作っておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/862c7bd6-1922-40a0-b47a-9726d3e23238.png)

PolyBevelSOPではそのグループ以外をベベルするようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/caf20d66-2adb-4584-a436-39e5fd9983d4.png)

これでアーティファクトは消えました。no_bevelエッジグループを作っている箇所をオンオフすると違いがわかります。レンダリングした結果も同様に改善されましたのでこれで解決とします。

![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/faed13f7-ef8e-4684-8652-76e5eef09ebc.gif)

:::note
このように一部だけBevelSOPの処理を除外するとサブディビジョンかけたときにおかしくなりませんか？デバッグ用ブランチにSubdivideSOPを置いたり、ヴューポートサブディビジョンで確認するようにしてますが、今回のもちょっと怪しいです。なんとなく４角形以上ができたときに問題が発生するような気がしています。本題ではないので、ここでやめますが、この辺りもどうするのが正解なのか知りたいところです。
![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951962/2b42beff-cdee-4e6d-8042-7604b1e3f955.gif)
:::

## まとめと今後
凹エッジを持つジオメトリーを押し出す時に発生する問題と、その対処についてまとめました。「なんだよ、この程度のこと記事にすんな」と思うかもしれませんが、私は今回もSOPの名前、パラメーターの意味、VEXの関数名、あれやこれや「ああ、あの時にやったあれだ。どうやったけなあ？」で結構時間を使いました。「この程度のこと」まで記事にして残しておかないと忘れてしまう人は私以外にもいると信じて「チュートリアルでつまづいたシリーズ」最初の投稿を終えます。今後も「この程度のこと」を懇切丁寧に記事にしていきますのでよろしくお願いします。