自己交差をなんとかしたい...という話

カーブやジオメトリへノイズを適用すると、カーブのもつれ、ジオメトリの裏返りに悩まされることがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/02dd2fd8-37f8-fe9b-f55b-218b4be64f89.png)

この記事はそれらを予防する方法ではなく、発生してしまった自己交差部分を除去するためのアプローチを検討したものです。

![issue.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/245a9feb-1251-3bdb-f226-f27be675eb73.png)

:::note
サンプルのシーンファイルは [こちら](https://drive.google.com/file/d/1yyLLu9zDV5f8ulT5snQHlmZvnJgFNT8j/view?usp=drive_link "approaches_to_remove_intersects.hiplc") からダウンロードできます。
:::

記事内ではアプローチの説明に終始し、実装都合の説明はサンプルシーン内へ記載する形を採っております。

# カーブの自己交差を除去

## 前提条件
元のカーブは枝別れしておらず、変形によって交差ができてしまった、という前提で話をすすめていきます。

![noise.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/fc992199-3e55-49e6-59b0-022e3bf57c0f.gif)

## 肝となるSOP
Find Shortest Path SOP を使用します。このノードは、指定した二点間の最短経路を見つけ出します。

## アプローチ

### 交差点の作成
交差する線分は、見た目では交差していてもジオメトリの構造上は繋がっていません。そのままでは Find Shortest Path が走査する際に交差点を考慮できないため、交差点を作る必要があります。

そのためには Intersect Stitch SOP を使用します。このノードはジオメトリの交差部分へ point を作成して縫合します。ただし、デフォルトでは検出精度が低いため、より精度を高めるために Proximity Tolerance 値を小さくします（処理は重くなります）。

下図の赤矢印は、Intersect Stitch 実行前後での、Find Shortest Path によって抽出されるパスのイメージです。

![stitch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/cf807a57-c0ea-b759-3446-1730abb80fa0.png)

### 最短経路の抽出
Find Shortest Path は経路探索の始点と終点 の point をグループとして指定することで機能するため、どの point へ start, end のグループを割り当てるかを考える必要があります。

#### カーブが開いている場合
この場合は非常にシンプルで、開いている場所（隣接pointの数が1の部分）を start, end にします。

![start_end.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/d1e929c9-c6fe-cafb-bf79-d1f66a59bf49.png)

#### カーブが閉じている場合
Find Shortest Path 実行後は、検出された道のり以外は残りません。そこで次のような手順を踏みます。

1. 閉じたカーブの一部を取り除き、開いている状態にする
1. 開いたカーブへ start, end を割り振り、Find Shortest Path を実行
1. 取り除いていた部分を元にもどす

![make_open_curve.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/9f0abdea-651c-7bd4-c6e6-de2659732814.png)

その際、取り除く場所はどこでもよいという訳ではありません。場所によっては下図のように望まない結果になり得ます。

![remove_prim.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/ffc8a915-34e9-8a20-2909-93bd22c1ffec.png)

完璧な解決策ではありませんが、交差点から交差点の道のりが最も長い一本道の、中ほどを取り除くと良さそうです。（交差点と交差点の道のりを測る方法は後述します。）

![perimeter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/3dd2823a-7d1a-4d3b-3736-12f1914a4ea3.png)

上の図は、交差点同士をつなぐ道のり毎に色分けし、それぞれの長さを表示したものです。この場合、最も長い道のりは右側の黄緑色の道で、その中ほどを取り除くことにします。

取り除かなかった部分へ Find Shortest Path を適用するため、取り除く部分は最小限にしたほうが良いでしょう。

![remove_small.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/88cc9d02-652a-f0d7-fb9e-242058293b3e.png)

この状態へ Find Shortest Path を適用し、その後に取り除いた部分を元に戻せば完了です。

### 交差点から交差点への道のりを調べる
交差点間の道のりを調べるには Measure SOP を使用します。

ただし Measure は各 primitive に対して処理を行うため、交差点から交差点までの道のりが一つの primitive である必要があります。

下図は交差点を含む道の各 primitive を色分けした例ですが、交差点毎に primitive が分かれていない状態です。(primitive 番号 0 が交差点をまたいでいます）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/394aa2b2-bfe2-22dc-6c56-402954740dca.png)

そこで Poly Path SOP を使用します。Poly Path はまさにこのためのノードで、交差点間を一つの primitive にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/7a516f97-5e46-de8f-6a13-ceed54bed86e.png)

こうすることで、交差点間の距離を Measure で取得することができるようになります。

![poly_path.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/272968a1-5042-ed76-ff2f-b919afff7ef8.png)

## 別アプローチによる自己交差除去
次に説明するアプローチは、最短経路以外の道のりを結果として得られることがあります。このアプローチでは Find Shortest Path を使用しません。

:::note warn
サンプルシーンでの実装は詰めが甘く、汎用的に使用できるものではありません。アプローチの参考程度としてご覧ください。
:::

### 自己交差の除去
図の自己交差について、道の流れが矢印のようであるとします。なお、このカーブへは既に Intersect Stitch を適用して交差点の縫合を行っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/9f1d9bf5-5231-447d-f8b4-56070b906d3f.png)

以下のようにして自己交差を取り除きます。

1. スタート地点から最初の交差点まで進む
1. 交差点をそのまま進み、再び最初の交差点に到達するまで進む
1. 2で通った道のりを削除する
1. 1へ戻る

![remove_path_recursive.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/1b7ce12b-0d5a-318c-235c-0acc963d63fd.png)

こうすることで、最終的に自己交差を取り除くことができます。
次項は、それを行うために留意することです。

### 交差点到達後の進行方向
交差点のほとんどは、２本のまっすぐな道が交差することで出来ているように見えます。

![next_path_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/e1c248ae-d3cc-3db3-23bf-51c35bd3f13b.png)

ですが下図のような道が Intersect Stitch によって接続された結果である場合もあります。

![next_path_actual.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/b9bfffc3-8e74-fea5-b00a-c1c7672884e0.png)

今回のアプローチで交差点侵入後に進む道は、Intersect Stitch を適用する前の道順と同じである必要があります。そうでないと辿った道のりを戻ることになりかねません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/95d3da83-ea1d-279d-e33f-ee5530dadee1.png)

#### Intersect Stitch 前の道順を利用する
下図の左は Intersect Stitch を適用する前（交差点作成前）の道のりで、primitive番号 （ピンクの数字）は道のり順に並んでいます。この ID をアトリビュートとして記憶させます（黄色の数字）。

図の右では、Intersect Stitch により primitive が再構築されたため primitive番号 は道のり順ではなくなっています。ですが記憶した数字（黄色の数字）の並びは壊れていません。

![record_orig_prim.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/65d01a88-503e-154f-97e7-03f813c3b589.png)

これを利用して、交差点侵入前の道が記憶していた数字を、交差点に接している他の道から探します。そうすれば、交差点到達後に進む道を間違えずにすみます。

このアプローチであれば、下図のように同じ番号が交差点で見つからないケースでも対応できます。同じ番号の道が見つからない場合、自分が持つ番号の前後を探せば問題ありません。

![correct_next_way.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/ecf2c528-4efa-9cb8-862d-ffb2ca3198f0.png)

#### primitive番号の整理
前項の内容は、Intersect Stitch 前の primitive 番号が道のり順になっている前提で成立しています。もし最初の時点で primitive 番号がバラバラになっている場合、Poly Path SOP と Convert Line SOP を使用することで、道のりの順に primitive 番号を並び替えることができます。

<dl>
  <dt>Poly Path SOP</dt>
  <dd>カーブの分岐と分岐の間を、一つの primitive にします</dd>
  <dt>Convert Line SOP</dt>
  <dd>point と point の間を一つひとつの primitive にします。その際、元カーブの道のり順に primitive 番号が割り振られます</dd>
</dl>

![clean_line_up.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/89d1423f-e55a-d683-4604-ca5cdf9f446a.png)

この Poly Path と Convert Line は、カーブのクリーンアップを行うためにセットで使用されることがよくあります。

## アプローチの違いによる比較

多くの場合は結果的に同じ出力になりますが、下図のような違いを得られることもあります。
下図の左が処理前のカーブ、右上がFind Shortest Path によるアプローチ、右下が別アプローチによるものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/7037e40f-8043-fa13-04e6-44837ce0f4ca.png)

# ジオメトリの裏返った部分を除去

## 前提条件
ボーダーエッジのない、閉じた形状のジオメトリであるという前提で話を進めます。

## 肝となるSOP
意外にも？Boolean SOP 単体で裏返り部分を除去できます。

![boolean2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/95618cd0-b38f-8c02-3dd3-9a842a3adfe8.png)

自己交差を取り除くという本来の目的はこれで解決しますが、この図でも確認できるように細かな破片が残ったり、他にも極端に尖ったエッジが残ったりします。下記はそれらを掃除するアプローチです。

## トポロジが大きく変わっても構わない場合のアプローチ
シルエットがある程度維持できれば良い場合のアプローチです。

細かなディティールは失われますが、この次に紹介するアプローチに比べて不具合がなく、高速に処理できます。

### ボリュームの利用

VDB from Polygons SOP によるボリュームへの変換と、Convert SOP または Convert VDB SOP によるポリゴンへの変換により、尖りすぎた形状を除去できます。

ただし、面の表裏が考慮されない結果になるため、自己交差を除去するには先に Boolean を適用しておく必要があります。

![pre_boolean.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/e9836a66-f9f9-7c51-6864-247e7c0874e6.png)

なお、VDB from Polygon と Convert VDB の組み合わせは、Remesh to Grid SOP でも代用できます。

### 細かな破片の除去

複雑な形状の場合、Boolean や ボリュームの適用後にさまざまな大きさの破片が残ります。最も大きな破片を残すために、破片毎の表面積を取得し、最大の表面積ではない破片を除去します。

そのためには、以下の各 SOP を使用します。

<dl>
  <dt>Connectivity SOP</dt>
  <dd>繋がっているジオメトリ毎に、番号を割り振ります。同じ破片の primitive は、同じ番号を持ちます。</dd>
  <dt>Measure SOP</dt>
  <dd>番号が同じ primitive の合計面積を取得します。つまり、破片毎の合計面積を取得します。</dd>
  <dt>Attribute Promote SOP</dt>
  <dd>各破片の合計面積のうち、最大値を記憶します。</dd>
  <dt>Attribute Wrangle SOP</dt>
  <dd>破片の面積が最大面積でないものを削除するために使用します。</dd>
</dl>

![extract_biggest_piece.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/9abcbaf2-1aeb-3edf-25f0-02ca16908a69.png)

### 結果
このアプローチを採った場合の結果は図の通りです。

![result_volume.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/cd1aef0b-1f14-5fee-97b5-20d309f53b6f.png)

## トポロジを出来るだけ維持したい場合のアプローチ

### 鋭利な部位の除去
全体的に Boolean を適用した部分は、鋭利な形状が残りがちです。

![remain_too_sharp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/43c03971-e59a-a858-10d5-cd9b7116a653.png)

また、極端に細くなった部位によって、塊同士が繋がってしまう場合があります。

![too_thin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/ea4a8391-9361-dae2-1c90-7e9223ba4f39.png)

これらを除去するために以下を行います

1. Boolean を適用した部分に接する場所を把握（Boolean と関係ない元々の形状を処理しないようにするため）
1. その中で、鋭利な形状や厚みが少なすぎるとみなせる部分を把握
1. 2で把握した部分を除去
1. 除去によって開いた穴を閉じる

#### 1, Boolean を適用した部分に接する場所を把握

Boolean SOP の A-B Seams パラメータにより、裏返りを除去した部分へ Group を残すことができます。

![boolean_seams.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/415a907f-f24d-47e9-7275-2ca06e8d9f7a.png)

#### 2, 鋭利な部分、厚みの少なすぎる部分を把握

この判定には、Measure Thickness SOP を使用します。このノードは、point から計測したジオメトリの厚さ情報を取得します。

![thickness.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/464140f5-9ad6-8f3f-dd19-d408f4cebf61.png)

前項で把握した裏返り除去部分にあたる point が、小さすぎる thickness 値を持っている場合、その部分を除去の対象とします。

#### 3, 4, 鋭利な部分、厚みの少なすぎる部分を修正

除去の対象となる point だけでなく、その隣接した primitive ごと除去し、 Poly Fill SOP で穴埋めします。つまり面を張りなおします。

![remake_prim.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/8e4683b6-8466-ffbc-e5a3-e6b16236c6f6.png)

問題点として、この段階で不正なジオメトリが発生していると Poly Fill が上手く機能しません。その前に Poly Doctor SOP で不正部分を修正する必要があります。

### 極端に尖った point をならす

ここまでの処理でも取り切れない、極端に尖った point が残ることはあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/e721b148-8864-3361-f6af-ece5040da3de.png)

これを近隣の point となじませるため、次のようにします。

1. 各 point の場所から、接している primitive の重心へ向かうベクトルを集める
1. それらの平均ベクトルと各ベクトルの角度を、内積を使用して調べる
1. どの角度も小さい場合、とても尖っている point だとみなす
1. その point の位置について、Attribute Blur SOP で周辺となじませる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/2ead9f5f-0d3b-d721-0324-75d8237b216a.png)

### For-Loop With Feedback による繰り返し処理

上記の処理を一通り行っても、primitive の削除や再生成、point 位置の調整を行ったことによる新たな自己交差が発生する可能性はあります。

それでも、これまでの一連の処理を繰り返すことで少しずつ自己交差は減り、そのうちに自己交差のないジオメトリになります。

### 結果

このアプローチを採った場合の結果は図の通りです。

![result_topology.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/e17d7ae4-5a0a-935b-c8af-d269d7ab4ac0.png)

## Boolean が使えない場合の裏返り除去

あまりに複雑なジオメトリでは Boolean が失敗したり、ポリゴン数が多すぎて処理に時間のかかりすぎることがあります。その場合のために別のアプローチを紹介します。

このアプローチではトポロジは維持されず、シルエットも他のアプローチと比べて滑らかになってしまいますが、安定して結果を得ることができます。

### 肝となるSOP

Point Cloud Surface SOP を使用します。この SOP は主にフォトグラメトリで使用されるもので、法線を持つ点群をもとにポリゴンを作成します。

![pcsurface.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/2abcc0d2-ad62-c3b2-f074-4bfbee1fc3a4.png)

また、Point Cloud Surface は裏返っていると判断されるような部分を生成対象から除外します。

### アプローチ

非常にシンプルで、以下のように行います。

1. Scatter SOP で、ジオメトリから沢山の点を生成
1. Attribute Wrangle SOP で、点群へ直近のジオメトリが持つ法線を転写
1. Point Cloud Surface SOP でメッシュを生成

### 結果

このアプローチを採った場合の結果は図の通りです。

![result_pccloud_surface.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/71b74a40-730f-3fb4-d6b5-1e98ea92aaf3.png)

## アプローチの違いによる比較

左上が処理前のジオメトリ、右上がボリュームを使用したアプローチ、左下がトポロジの維持を重視したアプローチ、右下が Point Cloud Surface SOP を使用したアプローチです。

![compare_approaches.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3930709/7d3a69c0-6366-df30-0494-0495aadb18c7.png)

このうち、不正ジオメトリによるエラーが最も少ないのは Point Cloud Surface を使用したものになります。

# 最後に

Houdiniは検証をし易く、試行錯誤する際のイテレーションを早く回せるのが強みだと思います。
また、意外なノードが意外な用途に役立つことも多いため、色々なアプローチを模索してみることが新たな思い付きと発見につながります。

事実、この記事の最終確認をしている段階で、Point Cloud Surface SOP の挙動について「まさか......」と思いついたことがありました。試してみると予想通りで、土壇場で記事の修正を行いました。

それを思いつく前のアプローチも [次の記事](https://qiita.com/vxlxv54/items/2cf5b84e9b180e7b4ba2) へ残しております。そのアプローチは遠回りで、かつ仕上がりも悪いものですが、何らかのヒントにでもなればと思います。
（サンプルシーン内でも、approach_mesh OBJ 内の右端へ omitted_approach ノードとしてひっそりと置いています。）

それでは、良い年末を。