みさご解体新書

ベジェ曲線

解説/アルゴリズム

二次ベジェ曲線

A B C 3つの点ABCを用意して、点AB、点BCを直線で結んで2つの線を作る。 A B C D E

ここで線 AB の線形補間、線 BC の線形補間を行う。

補間係数の値は何でもいいのだが、とりあえず 0.3 を設定する。

つまり、A ~ B までの距離を 30% 進んだ場所と、B ~ C までの距離を 30% 進んだ場所の位置を求める。(上記の D、E 地点)

A B C D E

線形補間で求めた位置同士を線で結ぶ。

A B C D E F

その結んだ線から更に線形補間をする。補間係数の値は上と同じで 0.3。

つまり、D ~ E までの距離を 30%進んだ場所を求める。(上記の F 地点)

この求めた F 地点の位置が二次ベジェ曲線で描かれる曲線の位置になる。

A B C

補間係数を 0.0 ~ 1.0 まで求めて繋げて軌跡にすると、二次ベジェ曲線を描くことができる。

三つの点から二本の直線を描き、二つの線形補間の点を求める。
二つの点から一本の直線を描き、一つの線形補間の点を求める。

まとめると二次ベジェ曲線のアルゴリズムは上記の通りとなる。

N個の点からN-1本の直線を描き、N-1個の線形補間の点を求める
(点が一つになるまでループ)

一般化すると、点の数をいくらでも増やし複雑なベジェ曲線を描くことができる。

三次ベジェ曲線

点の数が 4 つの三次ベジェ曲線。

コード例

function drawBezier(a: Point, b: Point, c: Point): void {
  let px = a.x;
  let py = a.y;

  const max = 100;
  for (let i = 1; i <= max; i++) {
    const t = i / max;
    const ab = lerp(a, b, t);
    const bc = lerp(b, c, t);
    const d = lerp(ab, bc, t);
    p.line(px, py, d.x, d.y);
    px = d.x;
    py = d.y;
  }
}

線形補間による二次ベジェ曲線を描くコード例。

ベジェ曲線の特徴

  • 与えられた点のうち、最初の点を始点、最後の点を終点、間にある点を制御点と呼ぶ。
  • N 個の点からできる曲線は N-1 次曲線となる。

線形補間で一つの点が求まるまで計算するのではなく、一つの式で計算する方法がある。

点 ABC、補間係数 t が与えられている状態で二次ベジェ曲線を描くとする。

  • 点 AB 間の補間係数 t による線形補間の式は a×(1t)+b×ta \times (1 - t) + b \times t
  • 点 BC 間の補間係数 t による線形補間の式は b×(1t)+c×tb \times (1 - t) + c \times t

この 2 つの式から求まる点から更に線形補間をするので、

(a×(1t)+b×t)(1t)+(b×(1t)+c×t)×t(a \times (1 - t) + b \times t)(1 - t) + (b \times (1 - t) + c \times t) \times t

が、求めたい点の位置になる。

function calcBezier(a: number, b: number, c: number, t: number): number {
  return (a * (1 - t) + b * t) * (1 - t) + (b * (1 - t) + c * t) * t;
}

function drawBezier(a: Point, b: Point, c: Point): void {
  let px = a.x;
  let py = a.y;

  const max = 100;
  for (let i = 1; i <= max; i++) {
    const t = i / max;
    const x = calcBezier(a.x, b.x, c.x, t);
    const y = calcBezier(a.y, b.y, c.y, t);
    p.line(px, py, x, y);
    px = x;
    py = y;
  }
}

上記で求めた式を利用した二次ベジェ曲線を描くコード例。

三次以上も同じように計算すればいいのだが、かなりややこしい計算になるので、もう少し楽な計算方法を試してみる。

(x+y)n(x+y)^n 展開した式
(x+y)1(x+y)^1 x+yx+y
(x+y)2(x+y)^2 x2+2xy+y2x^2+2xy+y^2
(x+y)3(x+y)^3 x3+3x2y+3xy2+y3x^3+3x^2y+3xy^2+y^3
(x+y)4(x+y)^4 x4+4x3y+6x2y2+4xy3+y4x^4+4x^3y+6x^2y^2+4xy^3+y^4

(x+y)n(x+y)^n二項定理などを利用して展開すると上記のような式になるが、ここで x を (1t)(1-t) 、 y を tt に置き換える。

次数 x を(1t)(1-t)、y をttに置き換えた場合
一次 (1t)+t(1-t)+t
二次 (1t)2+2(1t)t+t2(1-t)^2+2 \cdot (1-t) \cdot t+t^2
三次 (1t)3+3(1t)2t+3(1t)t2+t3(1-t)^3+3 \cdot (1-t)^2 \cdot t +3 \cdot (1-t) \cdot t^2 + t^3
四次 (1t)4+4(1t)3t+6(1t)2t2+4(1t)t3+t4(1-t)^4+4 \cdot (1-t)^3 \cdot t+6 \cdot (1-t)^2 \cdot t^2+4 \cdot (1-t) \cdot t^3+t^4

この置き換えた式が実際にベジェ曲線で使用される式で、初項 * 点 A, 第 2 項 * 点 B, 第 3 項 * 点 C...と項ごとに位置を掛ければ、補間係数 t における位置が算出できる。

例えば点 ABCD を渡して三次ベジェ曲線の位置を求める場合は、下記の式になる。

A(1t)3+B3(1t)2t+C3(1t)t2+Dt3A \cdot (1-t)^3 + B \cdot 3 \cdot (1-t)^2 \cdot t + C \cdot 3 \cdot (1-t) \cdot t^2 + D \cdot t^3

function calcBezier3(a: Point, b: Point, c: Point, d: Point, t: number): Point {
  const x =
    a.x * (1 - t) ** 3 +
    b.x * 3 * (1 - t) ** 2 * t +
    c.x * 3 * (1 - t) * t ** 2 +
    d.x * t ** 3;
  const y =
    a.y * (1 - t) ** 3 +
    b.y * 3 * (1 - t) ** 2 * t +
    c.y * 3 * (1 - t) * t ** 2 +
    d.y * t ** 3;
  return { x, y };
}

function drawBezier3(a: Point, b: Point, c: Point, d: Point): void {
  let px = a.x;
  let py = a.y;
  const max = 100;

  for (let i = 1; i <= max; i++) {
    const t = i / max;
    const point = calcBezier3(a, b, c, d, t);
    p.line(px, py, point.x, point.y);
    px = point.x;
    py = point.y;
  }
}

ソースコード

point.ts / app.ts