Houdiniでパーツ干渉チェッカーを作った話

# はじめに

この記事は[Houdini Apprentice Advent Calendar 2024](https://qiita.com/advent-calendar/2024/happrentice) 12日目の記事です。

はじめまして！
フィギュア原型師をしております空都凪はとです！

メインツールはZBrushなのでHoudiniはサブウェポンな感じですが、Houdini触るのが大好きです！

今回はタイトルの通り、Houdiniでパーツの干渉をチェックする仕組みを作ったお話を書こうと思います。

まだまだ実務でバリバリ使えているわけではない若輩者故、お見苦しい部分もあるかと思いますが、知らないソフトに興味があるデジタル造形分野の皆様、違う分野での使われ方に興味があるHoudinistの皆様、温かい目で見ていただければ幸いです。

# 発端と目的
フィギュアの原型データ作成の中で、分割という大変な工程を終えた後、いざ出力パーツを出力したらダボや分割面の引き忘れでパーツがはまらない...というのは誰もが考えたくないものだと思います。
ましてや出力の仕事をしている時であればなおさらです。

前職でそんな失敗を経験したさい、自動でチェックできる仕組みがあればいいのになと思ったのが発端です。

わたしはアプリケーションの開発ができるわけではなかったため、「自動化」が可能なHoudiniでそのチェックができる仕組みを作ることを目的に開発をしてみました。

# 全体の流れ
まずは今回作ったものの流れを軽く説明します。
![2024y12m10d_145702888.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/eb517353-f270-cd1c-2262-f63d9f40b27b.png)

1.判定したい分割済みオブジェクトをインポート
↓
2.ゼロクリアランス対策で少しだけ縮める
↓
3.交差判定を行う
↓
4.3で行った判定の際に付与したアトリビュートをもとに色を付ける


こうして段階ごとにまとめて書くと、やっていること自体はかなり単純だと認識できるはずなのに、いざ仕組みを作るとなると、なかなか大変な部分もあったので、そのあたりも紹介できればと思います。

今回は実用性を考えて2通りの表示方式を作成しました。
ある1つのメッシュに着目した場合、交差そのものがあるかどうか、またそれがどれと交差しているのかを視覚的にわかるようにしています。
![2024y12m10d_153307195.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/aa2555dc-7078-ae1f-46d7-0378428af7db.png)

先の例では単純なスフィア同士で判定を行っていましたが、実際の分割済みのフィギュアデータでも検証を行っています。（こちらは2つ目の表示方式です）
![2024y12m10d_152823268.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/cbf9a98b-2d8f-bcfa-0387-d93845c7d485.png)

### 入力
分割してSTLとして書き出したデータ群を読み込み、それぞれに`class`アトリビュートを付与したものをインプットとして使用します。
複数のファイルの読み込みで[Filemerge](https://www.sidefx.com/ja/docs/houdini/nodes/sop/filemerge.html)を使用する場合、File Name Attributeにチェックを入れて、ファイルごとに`name`アトリビュートを付与しておくと扱いやすいです。

![2024y12m10d_160316982.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/b38aa2c4-f300-fd83-1321-e9e8c37b67ac.png)
恐らくHoudiniの仕様だと思いますが、STL形式のデータを読み込むとポリゴンが反転してしまっているので[reverse](https://www.sidefx.com/ja/docs/houdini/nodes/sop/reverse.html)を繋ぎ、[enumerate](https://www.sidefx.com/ja/docs/houdini/nodes/sop/enumerate.html)を使って`name`アトリビュートから`class`アトリビュートを作成します。

### 少しだけ縮める
交差判定を始める前に、それぞれのメッシュをほんの少しだけ縮めます。
今回作成したシーンでは0.1mmずつ縮めています。

これには後の交差判定の工程で使用する[Intersection Analysis](https://www.sidefx.com/ja/docs/houdini/nodes/sop/intersectionanalysis.html)の性質が関わってきます。
Intersection Analysisは、自己交差は無視したうえで2つのインプット同士の交差箇所をポイントとして出力してくれますが、この「交差箇所」には「交差はしていないが重なっている部分」も含まれてしまいます。

そのため、分割のブーリアンをゼロクリアランスで行っている場合は全体を少し縮めてやらないと、全体が接触していることになってしまいます。

![2024y12m10d_162328771.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/74a8d73b-3983-30b8-dc6c-d86bb7c08a0b.png)
左が縮小あり(出力ポイントなし)で右が縮小なし(出力ポイントあり)

最初の例のように単純なメッシュを扱うのであれば[peak](https://www.sidefx.com/ja/docs/houdini/nodes/sop/peak.html)を使うだけで済むのですが、ほぼすべてが複雑な形状をしているので、今回はコントローラーとして使用している[null](https://www.sidefx.com/ja/docs/houdini/nodes/sop/null.html)に「use_complex_offset」というトグルスイッチを設定したうえで、ボリュームを使用した収縮を実装しています。
![2024y12m10d_163007374.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/3461b1aa-5508-ad8a-b126-1bbfafecdf82.png)

操作自体はあくまで少量の収縮なので、
1.[VDB from Polygons](https://www.sidefx.com/ja/docs/houdini/nodes/sop/vdbfrompolygons.html)でパーツのメッシュをVDB化
↓
2.[VDB Reshape SDF](https://www.sidefx.com/ja/docs/houdini/nodes/sop/vdbreshapesdf.html)で均一に収縮
↓
3.[Convert VDB](https://www.sidefx.com/ja/docs/houdini/nodes/sop/convertvdb.html)でVDBをポリゴンに戻す
↓
4.3に対して元のメッシュを[Attribute Wrangle](https://www.sidefx.com/ja/docs/houdini/nodes/sop/attribwrangle.html)で被せる
という流れで行っています。

```VEX:VDBで収縮させたメッシュに元のメッシュを被せる
float maxdist = chf("maxdist");
float mix = chf("mix");

@d = xyzdist(1,@P,i@pr,v@uv);
vector trans = primuv(1,"P",@pr,@uv);
vector rest = @P;

@P = trans*mix + rest*(1.0-mix);
```

### 交差判定を行う
いよいよ肝である交差判定を行います。

これはパーツそれぞれに対して、それ以外のすべてとの交差があるかどうかを`Intersection Analysis`を用いて行い、その結果から必要な情報を引き出すという割と力業な手法を取っています。
![2024y12m10d_164946931.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/cb922190-6880-588a-10ee-0be736ea13ab.png)

画像内の`attributewrangle112`では`Intersection Analysis`で生成されたポイントが、どのメッシュとの交差によって生じたのかを探索しています。（交差しているメッシュの`class`アトリビュートを[xyzdist](https://www.sidefx.com/ja/docs/houdini/vex/functions/xyzdist.html)を使って検索する）
![2024y12m10d_165006966.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/c20fafe4-bce9-c3cf-2c4b-1a3de7a22a6d.png)
```VEX:生成されたポイントごとに、交差しているメッシュのclassアトリビュートを検索する
float maxdist = chf("maxdist");

@d = xyzdist(1,@P,i@pr,v@uv,maxdist);

if( @pr==-1 ){
removepoint(0,@ptnum);
}else{
i@contact=prim(1,"class",@pr);
}
```

次の`attributewrangle110`では、交差がの有無を判定する`detection`アトリビュートと交差しているメッシュの`class`アトリビュートをリスト化した`contact_list`アトリビュートを作成しています。
![2024y12m10d_165018039.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/d1e0d20c-f7d8-4cd9-701e-44a396df952b.png)
```VEX:
int contact_list[];

for( int i=0; i<npoints(1); i++ ){
    int contact = point(1,"contact",i);
    if(find(contact_list,contact)<0){
        append(contact_list,contact);
    }
}

int detection;

if(len(contact_list)>0){
    detection = 1;
}else{
    detection = 0;
}

i@detection = detection;

i[]@contact_list = sort(contact_list);


i@class = detail(2,"iteration");

```

### 色を付ける
ここまでで欲しい情報は出揃ったので、後は視覚的にわかりやすい表示にしていきます。
![2024y12m10d_170849394.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/119ea71e-3b6d-c7d9-2511-cab43eb7c0e2.png)

まずは`attributewrangle116`で、事前に[Rest Position](https://www.sidefx.com/ja/docs/houdini/nodes/sop/rest.html)で設定しておいた`rest`アトリビュートを使って縮ませていた形状をもとに戻します。
```VEX:縮める前の形状に戻す
@P=@rest;
```

##### 交差が有るか無いかだけで色分け(1つ目の手法)
`attributewrangle116`から左側に流れているものです。
こちらはごく単純で、[Group](https://www.sidefx.com/ja/docs/houdini/nodes/sop/groupcreate.html)を使って交差の有無を示す`detection`アトリビュートが1のものをグループを作り、それだけを[Color](https://www.sidefx.com/ja/docs/houdini/nodes/sop/color.html)で別の色で塗るというだけです。

![2024y12m10d_181712417.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/db3db4d1-1416-ccaa-cbd0-42261b5502ee.png)
ここでは
交差あり　→　赤
交差なし　→　青
という色分けになっています。

##### ある1つのメッシュに着目して周りがどうかも含めて色分け(2つ目の手法)
こちらは実質`attributewrangle117`1つだけです。
[Extract Centroid](https://www.sidefx.com/ja/docs/houdini/nodes/sop/extractcentroid.html)はポイントだけにした方がジオメトリスプレッドシート上で数値として確認しやすくなるためです。（こちらはスフィア群の例）
![2024y12m10d_171927519.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/2ba507e7-8820-49f1-00ef-1f34edbd3025.png)

メインとなる`attributewrangle117`では、着目するメッシュを`class`アトリビュートを使って指定し、それが交差判定を持っているかどうか（`detection`アトリビュートが1かどうか）、交差しているのであれば何と交差しているのか（`contact_list`に格納されているものはなにか）、この2つをもとに着色していきます。

```VEX:
vector focus_color_normal = chv("focus_color_normal");
vector focus_color_error = chv("focus_color_error");
vector contact_color = chv("contact_color");
vector unconnected_color = chv("unconnected_color");

int focus_num = chi("focus_num");
int contact_list[] = point(1,"contact_list",focus_num);

if( prim(0,"detection",@primnum)==1 ){

    if( @class==focus_num ){
        @Cd = focus_color_error;
        }else{
        if( find(contact_list,@class)>-0.5 ){
            @Cd = contact_color;
        }else{
            @Cd = unconnected_color;
       }
    }
}else{
    if( @class==focus_num ){
            @Cd = focus_color_normal;
        }else{
            @Cd = unconnected_color;
        }
}
```

こちらのコードでは、
着目しているものが交差判定を持っていない　→　緑
着目しているものが交差判定を持っている　　→　赤
着目しているものと交差している　　　　　　→　黄
それ以外　　　　　　　　　　　　　　　　　→　青
というような色分けになっています。

![2024y12m10d_173423003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/bab8ae95-7905-60e0-4b6b-b776a8490732.png)
![2024y12m10d_173500731.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/0e9d03ef-bea8-5fa6-01ff-768a69cb2ef2.png)

### コントローラーの作成
最後にこれらを扱いやすいようにコントローラーを作りました。
特に色の指定についてはカラーサークルを使えるのでかなり楽になりました。
![2024y12m10d_173807031.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/633375/1226ed87-87d4-eb70-b903-c7ce0b13dd06.png)

# 開発を振り返って
実は今回の開発自体はかなり前から構想があり、今回ようやく形にすることができました。

特に交差判定を行う部分は構想初期からアルゴリズムが二転三転して今に至っています。
最初は[ポイントのアトリビュートを持たせて複数のループを作ってnC2の組み合わせを作って](https://x.com/gennsounohato/status/1426645507229642752)処理しようとしていましたし、そのうえ[Boolean](https://www.sidefx.com/ja/docs/houdini/nodes/sop/boolean.html)の`Intersect`ではうまくいかないことに気づいて愕然としたりしていました。

[途中ChatGPTに聞きながらPythonを書いたり](https://x.com/gennsounohato/status/1864764566644887691)もしましたが結局はVEXでの処理に落ち着きました。

こうした試行錯誤を重ねていく毎に、自分が手を伸ばせる範囲が広がっていくことを感じられてとても楽しかったです。


# 最後に
まずはここまで記事を読んでいただきどうもありがとうございました。

今回の実装ではvdbの処理があったり、`Intersection Analysis`の処理の軽量化が出来なかったり等、計算効率という面で大きな課題が残る結果になりました。

今後は実務でも使えるようにしていきたいと思っているので、良い案を思いついたら改善していきたいです。
また、ここまで読んでくださった方の中で、改善案をお持ちの方がいらっしゃいましたら、わたしの[X(旧Twitter)](https://x.com/gennsounohato)までご連絡を頂けると幸いです。
ご要望があればhipファイルもお渡しします。

## 参考ページ（最終的に使用しなかったものも含む）
xyzdist()は何ができるのか？
https://support.borndigital.co.jp/hc/ja/articles/360008833434-xyzdist-%E3%81%AF%E4%BD%95%E3%81%8C%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%AE%E3%81%8B

primuv()は何ができるのか？
https://support.borndigital.co.jp/hc/ja/articles/360008837974-primuv-%E3%81%AF%E4%BD%95%E3%81%8C%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%AE%E3%81%8B

Move point to volume "surface"
https://www.reddit.com/r/Houdini/comments/159y4yr/move_point_to_volume_surface/

Volume Gradient VOP
https://houdinifx.jp/blog/volume-gradient-vop/

H16新機能 コンパイルブロック 1
https://houdinifx.jp/blog/h16%E6%96%B0%E6%A9%9F%E8%83%BD-%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF-1/

【ゼロから始めるHoudini】 10 - 処理の効率を考えよう
https://www.youtube.com/watch?v=S8Ynsvl-xSU

find VEX function
https://www.sidefx.com/ja/docs/houdini/vex/functions/find.html

hou.Geometry class
https://www.sidefx.com/ja/docs/houdini/hom/hou/Geometry.html#setArrayAttrib

array: remove same values?
https://www.sidefx.com/forum/topic/61012/

配列
https://www.sidefx.com/ja/docs/houdini/vex/arrays.html

【Python】リスト内の要素の全ての組み合わせを出力
https://qiita.com/Mohrey/items/b0afc8edc5fed742b68a

Houdiniで利用できるスクリプトについて
https://houdinifx.jp/blog/houdini%E3%81%A7%E5%88%A9%E7%94%A8%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6/

random_shash VEX function
https://www.sidefx.com/ja/docs/houdini/vex/functions/random_shash.html

I need a random color for the connected pieces...
https://forums.odforce.net/topic/47345-i-need-a-random-color-for-the-connected-pieces/

Houdini Basics - Using the Performance Monitor to Profile Nodes
https://www.youtube.com/watch?v=uJFuvTb0OSU

パフォーマンスモニタペイン
https://www.sidefx.com/ja/docs/houdini/ref/panes/perfmon.html

chatGPT:Houdini Python nC2 組み合わせ
https://chatgpt.com/share/675804f1-3bb8-8004-b3cd-623afd463e1e

chatGPT:配列に値を追加
https://chatgpt.com/share/67580556-9a3c-8004-8ee1-ea3d8055d454