Edge Slideを作る【モデリングツール】

***Houdini Advent Calendar 2022 7日目の記事です。***

ある日、こんな質問をもらいました。
「BlenderやMayaのEdge SlideみたいなことをHoudiniでしたいんだけど、何か方法ある？」
「パラメータは 0.0 ～ 1.0 の値で調整したい」

この記事は上記の問題を解決すべく立ち上がった、
一人のエフェクトアーティストの記録です。

実際の私の思考の流れに沿って解説しますので、
皆さんも一緒に考えながら読んでみてください。

#### 対象者
- [ ] SOPへの理解を深めたい人
- [ ] ネットワーク構築力を鍛えたい人

#### 目標
- [ ] BlenderのEdge Slideみたいな機能を実現する
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/023291dd-1428-1a28-4739-40c24deedf42.gif">



# 案1. Rayノードを使う（失敗）
:::note info
 ***Ray***
1番目の入力ジオメトリの各ポイントをそのポイントの法線方向で、
光線を2番目の入力ジオメトリに投影させて、それらのポイントを動かします。
https://www.sidefx.com/ja/docs/houdini/nodes/sop/ray.html
:::


最初に思い浮かんだのは、Rayノードを使う方法です。
どれだけポイントを動かしても必ずサーフェスに貼りつくので、
うまくいくと思って、試してみた結果がこちらです。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/b76544b5-bdbf-4100-14c8-05df978502cd.gif">
<br>
<details><summary>ノードネットワーク（表示する場合はクリックして下さい）</summary>
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/046e6cab-bd4b-c705-20f6-a40cc7d94226.png">
</details>

想定していた挙動とは大きく違っていて、ジオメトリも破綻しています。
オプションも見直してみましたが、理想に近づくことはありませんでした。

### 問題点：移動方向や距離を個別に制御できない
Rayノードはあくまで投影先にポイントを沿わせるノードであり、
***<font color="Red">各エッジの理想的な移動を担保してくれません。</font>***



# 案2. Edit Sopを使う

:::note info
***Edit（Slide on Surface）***
既存サーフェスのポイントやエッジを動かす時に、
そのサーフェス上を“滑る”ようにその編集を拘束をします。
https://www.sidefx.com/ja/docs/houdini/nodes/sop/edit.html
:::

次は「Houdini Edge Slide」などの検索をしていた時に見つけたEditノードを使う方法です。
<b><font color="Red">Slide on Surfaceにチェックを入れる</font></b>と、いい感じにエッジが移動してくれそうだったので
試してみました。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/1df59cf6-0175-aca2-f76f-83a2684569c6.gif">
<br>
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/d9d3223d-ffc6-10bf-87f8-29e843713aae.png">
<br>

<details><summary>ノードネットワーク（表示する場合はクリックして下さい）</summary>
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/50fbc25f-c793-5988-d23c-7698f167bff2.png">
</details>

上手くいきました。***「少しモデリングで使いたい」ぐらいの用途であれば
この方法で十分です***が、問題点もあります。

### 問題点：0.0～1.0でエッジの移動をコントロールできない
プリミティブのサイズによって、移動に必要なTranslateの値が大きく変わることや、
「0.0～1.0でコントロールしたい」という要望もあるので、Editノードでは不十分です。


# 案3. 自分で作る

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/2db6d39a-76be-a1b6-e59a-e75b27003fa6.gif">

既存のノードでは上手くいかず、HDAも見つからなかったので（※）、
自分で作ることにしました。

※ 有志の方が作られたotlはありましたが、一部不具合もあり、
　 長期間更新されていないことから、本記事では取り上げません。

完成したネットワークはこちらです。
詳しい説明は後ほどします。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/60018483-2609-0cc9-877f-6a6147b76b8e.png">

## 解説1. 仕組みをイメージする

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/3dce564f-3b67-7ce2-be2c-477160a87b8b.gif">

Edge Slideを作るにあたって、どういう仕組みにするのかをイメージします。
私は<b>選択したエッジAと<font color="Red">交差するエッジBを見つけて、エッジBに沿って</font>
エッジAを構成する<font color="Red">ポイントA'をスライドさせる</font></b>ことを考えました。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/cf8f8ea8-16a3-d1a7-7e51-e29305b0ce7e.png">


この仕組みを実現するために必要な要素は二つです
- [ ] ***交差するエッジBを見つける***
- [ ] ***エッジBに沿ってポイントA'をスライドさせる***

イメージが掴めたので、実装に取り掛かります。


## 解説2. テスト用モデルとエッジグループを用意する
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/42b0d3b9-a85f-2fd6-0da2-85883d0fab7a.png">

まずは動作を確認する為のモデルを用意しましょう。
**モデルの形状や選択するエッジは何でも構いませんが**、あまり単純すぎると
不具合を見逃したりするので注意して下さい。
<font color="Red">**Group Nameはslide**、 **Group TypeはEdges**</font>を選び、いくつかのエッジを選択します。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/62c94257-f7e6-1f2c-2c14-a0feb32b0650.png">


## 解説3. 選択したエッジAと交差するエッジBを見つける
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/f0d63e07-0d30-9200-6e3b-839c28c173b7.png">

エッジAと交差するエッジBは<b>`Group Expand Sop`</b>と
<b>`Group Combine Sop`</b>を使えば探し出せます。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/6ef3f172-fa01-9c47-2cb4-005500c70296.png">


<b>`Group Expand Sop`</b>でエッジグループの<b>範囲を広げ</b>、
<b>`Group Combine Sop`</b>で広がったグループから<b>元のグループを引く</b>

これだけです。

### パラメータの詳細

***■ Group Expand Sop***
　・Group Name：<font color="Red"><b>groupexpand1</b></font>
　・Base Group：<font color="Red"><b>slide</b></font>
　・Group Type：<font color="Red"><b>Edges</b></font>
　▼ Normal Constraints
　　・Restrict by Nomarl Spread Angle：<font color="Red"><b>180</b></font>

***■ Group Combine***
　・Group Name：<font color="Red"><b>groupexpand1</b></font>
　　Equals, <font color="Red"><b>groupexpand1</b></font>
　　<font color="Red"><b>Subtraction</b></font>, With, <font color="Red"><b>slide</b></font>



<details><summary>ここまでのノードネットワーク（表示する場合はクリックして下さい）</summary>
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/b0019240-3eea-fe76-493e-f8e0f17c22d0.png">
</details>

## 解説4. エッジBに沿ってポイントA'をスライドさせる

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/7dee4de8-d783-ddde-6458-40af2b1fd51c.gif">

まずは、<b>`Labs Edge Group To Curve Sop`</b>を使います。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/2ad87e4c-e88d-f5ba-b9bc-cc4aaa40c194.png">

<font color="Red">**Group**</font>を<font color="Red">**groupexpand1**</font>にすると、エッジBだけが抽出できます。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/90ea4741-abe1-7d49-59df-a76408e3cab8.png">

次はポイントA'をエッジBに沿ってスライドさせるために、
<b>`Attribute Wrangle Sop`</b>を作り、↓の様に繋ぎます。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/2b192ffc-cad6-9cf1-528a-7b17921744ae.png">

繋いだ後は、Attribute Wrangle Sopにコードを書いていくのですが、
その際に、VEX関数の<font color="Red">**xyzdist(), primuv()**</font>を使います。


<b>xyzdist()のイメージ</b>
Rayノードを使って、ポイントをジオメトリにくっ付けた後、
くっ付いた場所がジオメトリのUV空間のどの位置（uv）にあるのかを調べる。
（距離やプリミティブ番号も調べられます）

https://www.sidefx.com/ja/docs/houdini/vex/functions/xyzdist.html

<b>primuv()のイメージ</b>
ジオメトリ上で指定したUV座標の位置がワールド空間のどこ（P）にあるのかを調べる。
（@P以外のアトリビュートも調べられます）


https://www.sidefx.com/ja/docs/houdini/vex/functions/primuv.html


xyzdist()でポイントA'に一番近いエッジBの位置座標を、UV座標として取得した後、
取得したUV座標をずらし、primuv()を使ってUV座標を位置座標に変換して@Pに代入すると、
ポイントA'をエッジBに沿ってスライドさせることが出来るようになります。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/e3f367ad-8a3a-7d88-b241-46f372c29ab9.png">

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/7d9af048-e8a5-bbeb-061b-0a3fd207cd87.png">

しかし、まだ問題があります。
ポイントA'のスライドする方向がバラバラになってしまう事です。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/da979774-a338-6164-cd4a-7bc906b753a2.gif">

原因は、それぞれのエッジBに対して UV[0] が 0.0 のときの位置座標が違っているからです。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/b04b6d77-35d7-6f4d-e309-ba7d0d1cfbe0.png">


### パラメータの詳細

***■ Attribute Wrangle Sop***
　・Group ：<font color="Red"><b>slide</b></font>
```VEX:Attribute Wrangle
vector uv;
int prim;
// @Pに一番近い、エッジBの位置（プリミティブ番号＋UV座標）を取得する
xyzdist(1, v@P, prim, uv);

// @UV[0]の値をずらす
uv[0] += chf("shift");

// 指定したUVの座標を取得後、@Pに入れる
v@P = primuv(1, "P", prim, uv);
```







## 解説5. 問題点を洗い出す
先ほど、「UV[0] が 0.0 のときの位置座標が違っている」と書きましたが、
UVを貼り直して位置座標を合わせれば良いだけだと考えるかもしれません。

しかし、その解決方法には2つの乗り越えなければならない問題があります。

- [ ] <b>位置座標を合わせる基準が存在しない</b>
- [ ] <b>UVを編集できない</b>

<dl>
<dt>位置座標を合わせる基準が存在しない</dt>
<dd>

UVの向きを全て揃えるには、<b>エッジの始点となるポイントを決める必要があります。</b>
「Y座標が一番高いポイントを始点にしよう」などと考えるかもしれませんが、
エッジの角度は様々ですし、単純なソートで得られる値をそのまま基準には出来ません。
<font color="Red"><b>ベクトルの内積・外積を使って基準を作り出す必要があります。</b></font>
<br>
</dd>
<dt>UVを編集できない</dt>
<dd>

xyzdist()で取得できるUVは<font color="Red">**intrinsic primitive UVs**</font>と呼ばれるものです。
様々なVEX関数を使って値を取り出すことは出来るのですが、編集は出来ませんでした。
つまり、<b>理想的なUVを持つプリミティブを再生成する必要があります</b>。
今回、作り直しの対象であるエッジは既に`Labs Edge Group To Curve Sop`で抽出
されていますので、<b>`Add Sop`</b>を使ってもう一度エッジを生成し直すだけで済みます。
</dd>
</dl>

問題が把握できたところで、ネットワークの構築に進みます。


## 解説6. プリミティブを再生成する 
基準を作る前に、プリミティブを再生成するためのネットワークを組んでしまいます。

まずは<b>`Attribute Create Sop`を作り</b>、↓の場所に繋ぎます
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/ce0e205c-d8ff-1dd8-b665-1a6fa6c7d961.png">
<font color="Red">**Name**</font>は<font color="Red">**id**</font>, <font color="Red">**Type**</font>は<font color="Red">**Integer**</font>, <font color="Red">**Value**</font>は<font color="Red">**@ptnum**</font>とします。
@idは<b>アトリビュートをコピー</b>したり、<b>ポイントをソート</b>するのに使います。

次は<b>`Sort Sop`</b>と<b>`Add Sop`</b>を作り、
<b>`For-Eachブロック`</b>と<b>`Compileブロック`</b>で囲います。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/e839c9a8-5a52-72cf-a860-cd239705a877.png">
`For-Eachブロック`を作るときは、<b>For-Each Primitiveを選びます。</b>
また、`Compileブロック`で囲った後は、`For-Eachブロック`の<b>`Block End Sop`</b>で
<b>Multithread when CompiledのチェックをON</b>にすることを忘れないでください。

これでエッジを再生成することが出来るようになりました。
`Add Sop`ではポイント番号の順にエッジを生成していますので、
その前に、ソート基準を作り（@id）それに従ってポイント番号をソートする
必要があります（方法は後述します）。

その他のパラメータの値については↓のパラメータの詳細にて記載していますので、
そちらを参照ください。

### パラメータの詳細

***■ Attribute Create Sop***
　・Name ：<font color="Red"><b>id</b></font>
　・Type ：<font color="Red"><b>Integet</b></font>
　・Value ：<font color="Red"><b>@ptnum, 0, 0, 0</b></font>

***■ Sort Sop***
　・Point Sort ：<font color="Red"><b>By Attribute</b></font>

***■ Add Sop***
　・Delete Geometry But Keep the Points ：<font color="Red"><b>チェックON</b></font>

***■ For-Eachブロック***
　・タブメニュー ：<font color="Red"><b>For-Each Primitive</b></font>
　・Multithread when Compiled ：<font color="Red"><b>チェックON</b></font>

<details><summary>ここまでのノードネットワーク（表示する場合はクリックして下さい）</summary>
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/70b0526d-ab43-26fd-46f7-9ae14f277128.png">
</details>


## 解説7. ソート基準を作る 
これからソート基準を作るのですが、その前に知っておかなければならない
ベクトルの内積と外積について簡単に解説します。

<dl>
<dt>ベクトルの内積</dt>
<dd>

<font color="Red"><b>ベクトルAとベクトルBがどのぐらい似ているのかを-1.0～1.0の値</b></font>で示します。
`dot(ベクトルA, ベクトルB)`と書くと計算されます。
（↑の式の場合、ベクトルBがベクトルAにどれだけ似ているかが分かります）
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/45ced6fd-4f58-d0d7-e1b4-52350047e882.png">


<br>
</dd>
<dt>ベクトルの外積</dt>
<dd>

<font color="Red"><b>ベクトルAとベクトルBに対して直角なベクトルC</b></font>を示します。
cros(ベクトルA, ベクトルB)と書くと計算されます。
ベクトルCの向きはベクトルを掛けた順番によって変わるので注意してください。
（↓のように中指= cross(親指, 人差し指)と覚えると楽です。）
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/d0d53f46-b74f-2829-6941-fc99bf258bf4.png">

</dd>
</dl>

ベクトルの内積・外積が理解できたところで、ソートを作る方法を解説します。
それは「<font color="Red"><b>オブジェクトの法線(@N)と接線(@tangentu)を外積して出来たベクトルCと
同じ方向にあるポイントを始点にする</b></font>」です。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/a01ed896-2b98-c6e1-54bf-60ad7ac6e045.png">



目標が決まったのでネットワークを構築していきます。
まずは、<b>`Normal Sop`</b>を作り、ポイントに法線情報を持たせます。
その後、<b>`Labs Edge Group To Curve Sop`</b>と<b>`PolyFrame Sop`</b>、
<b>`Attribute Create Sop`</b>、<b>`Attribute Copy Sop`</b>を作り、↓のように接続します。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/9ebc3981-f8a9-f226-1ba2-57e4033965f8.png">

`Labs Edge Group To Curve Sop`で選択エッジのみを抽出し、
`PolyFrame Sop`でエッジの接線（@tangentu）を計算しています。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/37f764ca-3dda-1149-0085-749a36a1c42e.png">

`Attribute Create Sop`では選択エッジが中心であることを示すための
centerアトリビュートを作り、`Attribute Copy Sop`で@tangentu, 
@centerを元のジオメトリにコピーしています。

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/f05e9e20-f04a-c46e-91b9-836c868b9167.png">

次は<b>`Attribute Wrangle Sop`</b>と<b>`Attribute Create Sop`</b>を作ります。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/5bbaca24-5bf5-1e92-df4c-8e8ba8d5b2f5.png">

`Attribute Wrangle Sop`ではcenterアトリビュートをグループ情報に変換しています。
（グループはVEXで<b>`@group_グループ名 = 1`</b>と書いても作れます）
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/316197f1-c650-36c3-d7b6-c3bc675887a1.png">

`Attribute Create Sop`は既にある<b>idアトリビュートを全て1で上書き</b>しています。


全てのアトリビュートの準備が整いましたので、ソート基準を作成します。
<b>`Attribute Wrangle Sop`</b>を作り、↓のように繋ぎます。
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/ab846477-3f9f-35cd-112a-9257bf29ff0a.png">

`Attribute Wrangle Sop`の中では、始点と終点を求めるコードを書いています。
（@idの値が始点：0, 中間：1, 終点：2になるようにしています）

<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/160156cb-771a-9038-8175-49b90c5ee959.png">

これで理想的なポイントのソートが出来るようになり、
エッジのスライド方向も統一されました。

### パラメータの詳細

***■ Normal Sop***
　・Add Normals to ：<font color="Red"><b>points</b></font>

***■ Labs Edge Group To Curve Sop***
　・Group ：<font color="Red"><b>slide</b></font>

***■ PolyFrame Sop***
　・Normal Name ：<font color="Red"><b>チェックOFF</b></font>

***■ Attribute Create Sop (attribcreate1)***
　・Name ：<font color="Red"><b>center</b></font>
　・Type ：<font color="Red"><b>Integer</b></font>
　・Value ：<font color="Red"><b>1,0,0,0</b></font>

***■ Attribute Copy Sop***
　・Match by Attribute ：<font color="Red"><b>チェックON</b></font>
　・Attribute to Match ：<font color="Red"><b>id</b></font>
　・Attribute Name ：<font color="Red"><b>tangentu center</b></font>

***■ Attribute Wrangle Sop（make_center_grp）***
```VEX:Attribute Wrangle
i@group_center = i@center;
```

***■ Attribute Create Sop（attribcreate3）***
　・id ：<font color="Red"><b>id</b></font>
　・Type ：<font color="Red"><b>Integer</b></font>
　・Value ：<font color="Red"><b>1,0,0,0</b></font>

***■ Attribute Wrangle Sop（make_id_from_inner_outer_pt）***
　・Group ：<font color="Red"><b>!center</b></font>
```VEX:Attribute Wrangle
// center以外の全てのポイントで実行される

//　自身から一番近いcenterのポイント番号を取得
int center_pt = neighbour(0, @ptnum, 0);

// centerに格納されている各アトリビュートを取得
vector center_pos = point(0, "P", center_pt);
vector center_nml = point(0, "N", center_pt);
vector center_tnj = point(0, "tangentu", center_pt);

// centerから自分（ポイント）に向かうベクトルを作る
vector tnj = normalize(@P - center_pos);

// centerの法線と接戦の外積から始点方向を指すベクトルを作る
// 自分に向かうベクトルと始点方向を指すベクトルの内積を取る
if(dot(tnj, cross(center_nml, center_tnj)) > 0)
{
    // 内積の結果が0より大きい場合、自分（ポイント）は始点である
    @id = 0;
}
else
{
    // 内積の結果が0より小さい場合、自分（ポイント）は終点である
    @id = 2;
}
```


## 解説8. 完成
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/2db6d39a-76be-a1b6-e59a-e75b27003fa6.gif">


<details><summary>全体のノードネットワーク（表示する場合はクリックして下さい）</summary>
<img width="450" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/60018483-2609-0cc9-877f-6a6147b76b8e.png">
</details>


# おわり
如何だったでしょうか。
かなり雑な実装になってしまいましたが、速度も求めてませんし、
個人的にはこのぐらいでいいかなと思っています。

もし最適化する場合は、全部をコードに書き直す必要がありますので
興味のある方は挑戦してみてください！

ここまで読んでいただきありがとうございました！