Bezier Clipping法で曲線同士の交点を出す

# ベジェ曲線同士の交点
3次ベジェ曲線同士の交点は最大で9点存在するがストレートには解けないため、
何かしらの方法で近似していく必要がある。(ちなみに片方が直線の場合は[式で解ける](https://www.particleincell.com/2013/cubic-line-intersection/))
今回Bezier Clipping法を用いて実装した際の仕組みと手順をメモしておく。

# Bezier Clipping法とは
[Bezier Clipping](http://nishitalab.org/user/nis/ourworks/BezClip/BezierClipping.html)
> Bezier ClippingはBezier曲線の性質を利用することで、交点計算を安定して求める方法
> Bezier曲線を分割する処理を再帰的に行うことからBezier Clipping 法と名ずけられた。当初は曲面のレイトレーシング法が目的でしたが、多項式の解法から３Ｄ物体のレンダリング（表示）まで種々の図形処理への応用が可能となった。

実際の実装にあたっては [こちらの論文](http://nishitalab.org/user/nis/cdrom/cad/CAGD90Curve.pdf)を参照した。

# もっと簡単な方法
ベジェ曲線を含む矩形同士が被っているか判定して、被っていたら2分割していくだけの手法もある。
十分に面積が小さくなった時点で分割をやめる。そこまで遅くもなさそう。
下記の凸包性とド・カステリョが分かれば組める。
<img width="701" alt="bc_subdivide.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/751d1e85-7d6b-eb98-0e39-9c9128df9f40.jpeg">


# 予備知識

#### 1. ベジェの凸包性 (convex hull property)
ベジェ曲線は制御点によって作られる凸包に必ず内包される。
上のような矩形ベースでの判定は線がちょっとはみ出したりしそうな気になるけどその心配はない。
言われれば当たり前な気もするが、Bezier Clippingにおいても重要な性質となる。

<img width="644" alt="bc_convexhull.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/a08adf5a-b481-e4c1-4763-83ccef5b5bf1.jpeg">


#### 2. ド・カステリョのアルゴリズム (De Casteljau's algorithm)
ベジェを任意の点で分割するときに用いる。 https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm
難しそうな名前だけれど、単にベジェ曲線を描く過程の補助線や制御点が、
そのまま分割時のハンドルやアンカーポイントとして使えてすごい、みたいな理解をしている。すごい。
使用時に注意すべきはt=0.5で分割したからといって長さが半分になるわけではないという点。
![bc_castejau2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/076669e0-fdaa-cd9c-8fde-c3dcac6e0c08.png)



# Bezier Clipping
上にも書いた[この論文](http://nishitalab.org/user/nis/cdrom/cad/CAGD90Curve.pdf)に沿って進める

#### 大まかな流れ
対象の片方の線を太らせて、そこにもう片方の含まれる部分だけ切り出す。
それを交互に行うことで、互いの範囲を絞っていく。
<img width="1001" alt="bc_clipping-flow.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/92b71a2d-be23-f167-3430-1e007b11a1de.jpeg">


まずは下の曲線Aと曲線Bについて、切り取り1回分の操作を考えてみる。
![bc_twolines.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/d1c250ec-2a75-4f46-ef68-760126f3d7b1.png)



#### 1. 曲線BのFat Lineを作る
まずは1回目の切る側(A)、切られる側(B)を決めて、切る側を太らせる。(FatLineと呼ばれる)
この「太った範囲」をどのように作るかを考えていく。

まず太らせる方のベジェの制御点B1, B2について、直線L(B0→B3)への**符号付きの距離**をそれぞれ求める(`D1`, `D2`)。
(参考: [直線と点の距離](https://mathtrain.jp/tentotyokusen) 分子の絶対値をとらずそのまま使う)
ベジェの凸包性から、`D1〜D2`幅に曲線が収まることは分かるが、このままだと無駄が多い。
![bc_fatline01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/a343b2d2-f1a0-5566-8cfb-16806edeaf91.png)


正確に曲線の頭頂部までの距離を求めてギリギリの幅を出すことは一応できるが、
計算コストに見合わないため、ここでは距離符号の組み合わせ(`D1`, `D2`)から
ベジェの大まかな形状を判定した上で、距離を定数倍して狭めることにする。

S時状のとき
![bc_fatline02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/33c961d4-43ed-68a3-7a59-51d651e9b0be.png)

円弧状のとき
![bc_fatline03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/1561eac5-9404-1da3-80ac-3473a971d107.png)




S時の場合は双方に**4/9倍**、円弧状の場合は最大値に**3/4倍**することで最大幅(`dmin`, `dmax`)とする。
(数字の根拠は理解及ばずなので元論文を参照)
どちらを使うかは**距離符号の一致/不一致(XOR)**で判定する。




#### 2. 曲線AのFat Lineとの交差範囲を求める
今度はB0→B3の線に対してAの各制御点からの距離を求め、
ベジェのパラメータ(t)と距離(d)のグラフを作る。
![bc_t-d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/e0548897-fcc4-402b-1380-b1e2b8cc18c6.png)


各点からの距離のグラフもまたベジェ曲線となり、凸包性が使える。
この凸包形状をそのまま用いて、先に求めたFat Lineの`dmin`, `dmax`と組み合わせ、
tの交差している範囲`tmin`, `tmax`を求める。
![bc_tmin-tmax.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/a5674b78-49bc-cea8-c1a0-4b175cb4897e.png)




#### 3. 曲線Aを被った範囲で切り取る
ド・カステリョを2回使ってtmin/tmaxの範囲で曲線Aの部分曲線を求める。
![bc_trim-a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/deb03744-5497-5769-7fae-bfce56c050fe.png)


#### 4. AとBを入れ替える
双方が十分に短くなるまでこれを繰り返す。


# ケース別ハンドリング
#### AがBのFat Lineに全て含まれる場合
![bc_handling_case01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/3626ae97-8576-d908-156e-5d4437f57ccd.png)

この場合、Aが全て切り取られてしまうのでそこで終了してしまう。
一方で、AとBを入れ替えればかなり効率よく切り取ることができる。
特に初回は入れ替えた場合と合わせて算出しておき、
より切り取られる量`(1-(tmax-tmin))`の大きい方を採用するのが吉とのこと。

#### 複数点での交差
![bc_handling_case02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/63db09ce-b396-8398-51e9-3f3a5ca14408.png)
ストレートに進んだ場合、この手法では1点にしか対応できないため、
複数点で交差している場合はどこかで曲線を分割して元の曲線を増やすしかない。
判別方法としては、AとBどちらで切っても減るtの割合が20%以下の場合は、
(範囲が被っている割合が大きいということなので) 長い方※を再分割して
AB′,AB″の新たな組み合わせとしてそれぞれ処理するようにする。
この判定は初回だけでなく、その後も再帰的に行う必要がある。

※長さは実際の長さではなく、元の曲線に対して残っているベジェのパラメータ(t)

#### 補足:様々な距離パターンの考慮
![bc_handling_case03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/9309bfc7-913d-d554-7541-a9ad92149a3d.png)
工程「２」で距離グラフの凸包とdmin/dmaxを組み合わせてtの範囲を出しているが、
「X」形のように必ずしも片方がもう片方を綺麗にまたぐ形ばかりではないため、
凸包の各辺と`dmin`,`dmax`との交点のみを求めても上手くいかないケースがあるので注意する。

結論としては凸包の4辺と`y=dmin`, `y=dmax`の2直線の交点を全て出し、
そこに頂点のうちd値が`dmin<=d<=dmax`であるものを加え、
最後にそれら全てのx座標の`min`,`max`を求めれば`tmin`,`tmax`となる。
別の関数等で凸包x凸包の交差エリアがスムーズに出せれば、それでも良いと思う。
(初歩的すぎて論文内には特に詳しく書かれておらず少しハマったので補足)

# 動作サンプル
HoudiniのVEXにて実装
徐々に分割・収束して最終的に全て点になる様子
![inter01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/c808947f-32c9-871c-e5d0-36ae891e9420.gif)
交点数が変化していく様子
![inter02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153494/1fdf795c-c6a5-3310-ea8d-27a2b83c2b4c.gif)


# 出典・参考
- [Tomoyuki Nishita - Beizer Clipping](http://nishitalab.org/user/nis/ourworks/BezClip/BezierClipping.html)
- [T W Sederberg and T Nishita - Curve intersection using Bezier clipping (1990)](http://nishitalab.org/user/nis/cdrom/cad/CAGD90Curve.pdf)
- [pomax - A Primer on Bézier Curves](https://pomax.github.io/bezierinfo/)