Continue(s)

Twitter:@dn0t_ GitHub:@ogrew

p5.jsのbezier()の引数の順番に悶々として、夜な夜な調べてたらベジェ曲線完全に理解してた。

あけましておめでとうございます。

■ 今回のきっかけ

新年を迎えても相変わらずだらだらとp5.jsをいじっていてふと気になったことがありました。 それは「curve()関数とbezier()関数の引数の順番がややこしい」ということです。

P1(x1, y1) ... 開始点
P2(x2, y2) ... 終点
C1(cx1, cy1) ... 開始点側の制御点
C2(cx2, cy2) ... 終点側の制御点

としたとき(それぞれがどういう意味を持つかは後ほど)に、

curve(cx1, cy1, x1, y1, x2, y2, cx2, cy2)
curve() - Reference p5.js

というような順番であるのに対して、

bezier(x1, y1, cx1, cy1, cx2, cy2, x2, y2)
bezier() - Reference p5.js

みたいな感じなんですよね。

これのせいで気軽にcurve()関数をbezier()関数に置換させることができません。

f:id:taiga006:20210115222654p:plain

本当は関数を置換するだけで黒線(curve()関数)を赤線(bezier()関数)のようにしたい(のに、青線になってしまう…)。

という、どうでもいい細かいところに引っかかったところから

「そもそもベジェ曲線ってどうなってるの?」
「p5.jsだけでなくIllustratorPhotoshopなんかのAdobe製品でも出てくるし?」
「CADとかも使わないけど、たしか関係あるよね?」

など疑問が湧いてきたのでベジェ曲線について調べてみました。

■ そもそも線分とは?

ベジェ曲線アルゴリズムについて調べたとき、おそらく検索結果のトップ3のうち1つはこちらの記事「一から学ぶベジェ曲線」(日本語)かと思います。(この記事を読む前に一読することを勧めます。)

もともとサンフランシスコに住んでいるエンジニアの方が書かれたこちらの記事Bezier Curves from the Ground Up - Zero Wind(英語)があるので、英語が得意な方はそちらを読まれるのが良いかと思います。

まずは、記事にもあるように2点を指す位置ベクトルに対して、それを t : (1-t) (0≦t≦1) で内分する点の集合という観点で「線分」を定義します。

 P = (1-t)P_0 + tP_1 (0 \leqq t \leqq 1)
f:id:taiga006:20210117203241g:plain GIF1

■ 2次のベジェ曲線

そして、今度は点を1つ増やして2つの線分それぞれに対して同じように任意の比率の内分点の集合として線分を引いてみます。


{\displaystyle 
\begin{eqnarray}
  \left\{
    \begin{array}{l}
P_{0-1} = Q = (1-t)P_0 + tP_1  \\
P_{1-2} = R = (1-t)P_1 + tP_2  \\
(0 \leqq t \leqq 1)
    \end{array}
  \right.
\end{eqnarray}
}
f:id:taiga006:20210117203844g:plain GIF2

さらに先程の求めたQ点,R点に対して同じように t : (1-t) (0≦t≦1) で内分させた点Sをプロットすると、なんと曲線が描けます。

 S = (1-t)Q + tR (0 \leqq t \leqq 1)

これが2次のベジエ曲線と呼ばれるものです。
(つまり、1次のベジエ曲線とは定義上では直線を描くものとなります。)

f:id:taiga006:20210117204404g:plain GIF3

点を1つ増やす(次数を上げる)ことで3次のベジェ曲線も簡単に描けます。
ここまでの数式から3次のベジェ曲線のベクトル方程式は以下のように導けます。

 P = (1-t)^3 P_0 + 3(1-t)^2 P_1 + (1-t)t P_2 + t^2 P_3  (0 \leqq t \leqq 1)

GIF4の細いピンクの線はp5.jsのbezier()関数の結果です。

f:id:taiga006:20210117205400g:plain GIF4

ベジェ曲線を実装する

1,2,3次のベジェ曲線の方程式から一般化させたn次のベジェ曲線の定義式が以下です。


{\displaystyle 
\begin{eqnarray}
  \left\{
    \begin{array}{l}
P(t) = \sum_{i=0}^n  B^{n}_{i} (t) P_i  \\
B^{n}_{i} (t) = {}_{n}C_i t^i (1-t)^{n-i}  \\
    \end{array}
  \right.
\end{eqnarray}
}
(0 \leqq t \leqq 1)

この場合、制御点の数は(n+1)個であることに注意してください。(1次のベジェ曲線が2点を結ぶ線分だったことを思い出しましょう。)

n_C_iはいわゆる二項係数と呼ばれるもので、それを使ったB(t) のような関数をバーンスタイン基底関数と呼ぶそうです。

ここでは背景にある数学的面白さはひとまず横においておくとして、定義がわかったのでアルゴリズム(ベジエ氏より以前にこのアルゴリズムを発見した人にあやかって「ド・カステリョのアルゴリズムと呼ばれている)をプログラムに落とし込んでいきたいと思います。

※ちなみに「ド・カステリョのアルゴリズム含めより詳細なベジェ曲線についての説明を読みたい方はこちらの「A Primer on Bézier Curves(ベジェ曲線入門)」という無料の電子書籍サイトが参考になるかと思います。(僕は途中で読むのを断念しました。)

くどくど書いてきましたが実装は非常にシンプルになります。
(折りたたんでいたソースコードを読んでくださった人はわかると思いますが、ここまでの実装はすべて1つずつ計算する力技でした。)

f:id:taiga006:20210117210934g:plain GIF5

いかがでしょうか。黒線がp5.jsで書いたベジェ曲線。赤丸が移動している軌跡が今回実装したベジェ曲線になります。

念の為、一応細かいところを解説しておくと、二項係数は以下の公式を使っています。

  \displaystyle{
 {}_{n}C_k = \frac{n!}{k!(n-k)!}
}

(高校数学を思い出しますね。)

■ ところでベジェ氏って何者?

ピエール・ベジェ氏は、フランスの自動車会社「ルノー」に所属していたエンジニアで1972年に車体の形状の設計をするにあたり、このベジェ曲線を提案・発表したようです。

ちなみに先ほど登場したド・カステリョ氏は当時同じフランスの自動車メーカーのシトロエンに在籍しており、ベジェ曲線の発表より先に研究を進めていたものの外に公表されませんでした。

そのため、現在では一般的にはベジェさんの名前が使われているという経緯があります。

f:id:taiga006:20210117175451p:plain

■ 最初の目的見失ってない?

そうでした。そもそも今回のきっかけはp5.jsのbezier()関数の引数の順番に引っかかったところから出発しました。しかしそれもGIF4とかを見ているとすでにこの順番であることは不思議ではありません。が、一応せっかくなのでp5.jsのbezier()関数の実装を見にいきます。

//////////////////////////////////////////////
// SHAPE | Curves
//////////////////////////////////////////////
p5.Renderer2D.prototype.bezier = function(x1, y1, x2, y2, x3, y3, x4, y4) {
  this._pInst.beginShape();
  this._pInst.vertex(x1, y1);
  this._pInst.bezierVertex(x2, y2, x3, y3, x4, y4);
  this._pInst.endShape();
  return this;
};

p5.Renderer2D.prototype.curve = function(x1, y1, x2, y2, x3, y3, x4, y4) {
  this._pInst.beginShape();
  this._pInst.curveVertex(x1, y1);
  this._pInst.curveVertex(x2, y2);
  this._pInst.curveVertex(x3, y3);
  this._pInst.curveVertex(x4, y4);
  this._pInst.endShape();
  return this;
};

github.com

bezierVertex()、curveVertex()がそれぞれどう定義されているかまで追いかけてもよいのですが、これだけでcurve()関数とbezier()関数の違いを理解するのには十分です。

そろそろ書くのに疲れたので主題ではないのでここでは深く追求しませんが、curve()関数の中身は「Catmull-Rom スプライン」と呼ばれる曲線です。そして、これのベースには「エルミート曲線」と呼ばれる曲線アルゴリズムを使用されています。

この「エルミート曲線」は入力に①開始点と②終点と③開始点ベクトルと④終点ベクトルの4つが必要なのですが「Catmull-Rom スプライン」は③④のベクトルの代わりに長い曲線の中の「開始点の前の点(制御点1)から開始点の次の点(=終点)への傾き」と「終点の前の点(=開始点)から終点の後の点(制御点2)への傾き」を入力に入れて中でゴニョゴニョします。(参考1参考2

それがp5.js(およびProcessing)の実装における(x1, y1) と (x4,y4) の座標というわけです。「長い曲線の中の4つの点に注目している」というのがポイントです。そうなると P1(制御点)->P2(開始点)->P3(終点)->P4(制御点)のように引数の順番は(cx1, cy1, x1, y1, x2, y2, cx2, cy2)であるわけです。

一方のbezier()関数はここまでくどくど説明してきたとおり、P1(開始点)->P2(制御点)で線形補間してP3(制御点)->P4(終点)で線形補間して…とやっていくので引数の順番は(x1, y1, cx1, cy1, cx2, cy2, x2, y2)となります。

お~、読んでくださっている人に伝わっているかどうかは怪しいですが僕は非常に納得できました。

申し訳ないので上の説明のイメージ図だけ置いておきます。右がcurve()関数、左がbezier()関数です。

f:id:taiga006:20210117202320p:plainf:id:taiga006:20210117202314p:plain

まとめ

すごく些細な疑問点からその背景にある興味深い数学の知識を身につけることが少しできました。 それと余談ですが、はてなブログで数式(TeX)を書く方法も今回学べました。(参考

参考文献

結局70枚見てやっと振り返れた今年撮った写真2020

この記事は今年撮った写真 Advent Calender 2020の22日目の記事の続きです。

前半はこちらからどうぞ。
taiga.hatenadiary.com

30枚では10月の上旬くらいまでしか振り替えれなかったので残り2ヶ月分、のんびり見返していきましょう~。

(引続き、PhotoShop等での編集をする前の写真をお送りします。)


f:id:taiga006:20201225235608j:plain 31. 2020/10/18 PENTAX ME (lens: Helios-44-2)

これ、Heliosの最大の魅力でもあるぐるぐるボケが結構きれいに出てて好き。

f:id:taiga006:20201225235739j:plain 32. 2020/10/18 PENTAX ME (lens: Helios-44-2)

免許持ってない人間の中で日頃から一番カーブミラーを見ている自信がある。

f:id:taiga006:20201225235839j:plain 33. 2020/10/24 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201225235952j:plain 34. 2020/10/24 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226000004j:plain 35. 2020/10/24 PENTAX SP (lens: Super Takumar 55mm 1.8F)

人っていろいろだなあ、というのが感じられます。中華街で撮ったものです。

f:id:taiga006:20201226000132j:plain 36. 2020/10/25 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226000229j:plain 37. 2020/10/25 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226000207j:plain 38. 2020/10/25 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226000952j:plain 39. 2020/10/27 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226001006j:plain 40. 2020/10/27 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226003933j:plain 41. 2020/10/31 PENTAX ME (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226003807j:plain 42. 2020/10/31 PENTAX ME (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226004320j:plain 43. 2020/11/3 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226004330j:plain 44. 2020/11/3 PENTAX SP (lens: Super Takumar 55mm 1.8F)

光に誘われてなんとなく撮った一枚なんですが好きです。

f:id:taiga006:20201226004340j:plain 45. 2020/11/3 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226005254j:plain 46. 2020/11/3 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226005306j:plain 47. 2020/11/3 PENTAX SP (lens: Super Takumar 55mm 1.8F)

46、47あたりは完全にフィルムがジャムってしまったのを無理やり撮ったものなんですが不思議な写真が撮れました。

f:id:taiga006:20201226005357j:plain 48. 2020/11/8 PENTAX ME (lens: Helios-44-2)

こーーーーーれ、やばくない?この半年で撮った写真ベスト10に入る。

f:id:taiga006:20201226005539j:plain 49. 2020/11/8 PENTAX ME (lens: Helios-44-2)

f:id:taiga006:20201226005411j:plain 50. 2020/11/8 PENTAX ME (lens: Helios-44-2)

彼女の後ろ姿。

f:id:taiga006:20201226005619j:plain 51. 2020/11/14 PENTAX ME (lens: Super Takumar 55mm 1.8F)

これもいいよね、曇ってたのがちょっと残念。

f:id:taiga006:20201226005754j:plain 52. 2020/11/15 PENTAX ME (lens: Super Takumar 55mm 1.8F)

なんか北海道だかにある有名な湖みたいな綺麗な色合いですが、完全に水質がイッちゃってる大岡川です。

f:id:taiga006:20201226005814j:plain 53. 2020/11/15 PENTAX ME (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201226005836j:plain 54. 2020/11/15 PENTAX ME (lens: Super Takumar 55mm 1.8F)

3つの赤。映えるね~。

f:id:taiga006:20201226005900j:plain 55. 2020/11/15 PENTAX ME (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201227230707j:plain 56. 2020/11/19 PENTAX ME (lens: Super Takumar 55mm 1.8F)

みなとみらいで一番好きなスポット。

f:id:taiga006:20201227230835j:plain 57. 2020/11/19 PENTAX SP (lens: Helios 44-2)

f:id:taiga006:20201227230905j:plain 58. 2020/11/19 PENTAX SP (lens: Helios 44-2)

偶然手に入れたACROSS2。モノクロデビュー。難しかった~。

f:id:taiga006:20201227231010j:plain 59. 2020/11/28 PENTAX ME (lens: Super Takumar 55mm 1.8F)

この影!!!僕の写真垢のほうでも評判の良かった1枚です。

f:id:taiga006:20201227231044j:plain 60. 2020/11/28 PENTAX ME (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201227231317j:plain 61. 2020/12/5 PENTAX ME (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201227231433j:plain 62. 2020/12/5 PENTAX ME (lens: Super Takumar 55mm 1.8F)

なんかいい感じにフレーミングが面白くなったやつ。

f:id:taiga006:20201227233026j:plain 63. 2020/12/10 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201227231543j:plain 64. 2020/12/10 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201227231623j:plain 65. 2020/12/10 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201227231755j:plain 66. 2020/12/19 PENTAX SP (lens: Super Takumar 55mm 1.8F)

12月中旬の東京駅。赤いコートの女性が二人いてパシャリ。

f:id:taiga006:20201227231831j:plain 67. 2020/12/19 PENTAX SP (lens: Super Takumar 55mm 1.8F)

f:id:taiga006:20201227232919j:plain 68. 2020/12/19 PENTAX SP (lens: Super Takumar 55mm 1.8F)

近所で見つけた素敵な純喫茶。

f:id:taiga006:20201227232130j:plain 69. 2020/12/24 PENTAX SP (lens: Super Takumar 55mm 1.8F)

手前の木の枝のウネウネ具合と、電線の直線のバランス。

f:id:taiga006:20201227231912j:plain 70. 2020/12/24 PENTAX SP (lens: Super Takumar 55mm 1.8F)

奥の建物に映る木の影に吸い込まれそうになった。


という感じでした!

来年もいろいろ撮影していきたいと思います!!

では、また。