Chapter 33

ベジェ曲線

miku
miku
2021.11.23に更新

ベジェ曲線は曲線を描くアルゴリズムのうちの1つで、p5.jsでもベジェ曲線を扱う関数が幾つか定義されている。まずはベジェ曲線のアルゴリズムについて解説する。

2次ベジェ曲線

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

ここで線ABの線形補間、線BCの線形補間を行う。補間係数の値は何でもいいのだが、とりあえず 0.3 を設定する。つまり、A~Bまでの距離を30%進んだ場所と、B~Cまでの距離を30%進んだ場所の位置を求める。(上記のD、E地点)

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

その結んだ線から更に線形補間をする。補間係数の値は上と同じで 0.3。つまり、D~Eまでの距離を30%進んだ場所を求める。(上記のF地点)この求めたF地点の位置が2次ベジェ曲線で描かれる曲線の位置になる。

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

3つの点から2本の直線を描き、線形補間で2つの点を求める。
2つの点から1本の直線を描き、線形補間で1つの点を求める。

まとめると2次ベジェ曲線のアルゴリズムは上記の通りとなる。補間係数が 0 のとき始点、1 のとき終点の位置になり、間の点にはほとんどの場合において通ることがない。なので始点と終点以外の点のことを制御点と呼ぶ。

2次ベジェ曲線の実装例

function setup() {
  createCanvas(windowWidth, windowHeight);
  noFill();
  stroke(240);

  const space = 20;
  const a = { x: space, y: height - space };
  const b = { x: width / 2, y: space };
  const c = { x: width - space, y: height - space };

  drawBezier(a, b, c);

  drawCircle(a);
  drawCircle(b);
  drawCircle(c);
}

function drawBezier(a, b, c) {
  let prev = a;

  const n = 100;
  for (let i = 0; i <= n; i++) {
    const t = i / n;
    const d = lerp2d(a, b, t);
    const e = lerp2d(b, c, t);
    const f = lerp2d(d, e, t);
    line(prev.x, prev.y, f.x, f.y);

    prev = f;
  }
}

function drawCircle(pos) {
  push();
  strokeWeight(2);
  fill("#292a33");
  circle(pos.x, pos.y, 10);
  pop();
}

function lerp2d(a, b, t) {
  return { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) };
}

アルゴリズムの一般化

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

一般化すると、点の数をいくらでも増やし複雑なベジェ曲線を描くことができる。点の数がN個で描かれるベジェ曲線の事をN-1次ベジェ曲線と呼ぶ。

3次ベジェ曲線の作例

function setup() {
  createCanvas(windowWidth, windowHeight);
  noFill();
  stroke(240);

  const space = 20;
  const a = { x: space, y: height - space };
  const b = { x: width / 3, y: space };
  const c = { x: (width / 3) * 2, y: height - space };
  const d = { x: width - space, y: space };

  drawBezier3(a, b, c, d);

  drawCircle(a);
  drawCircle(b);
  drawCircle(c);
  drawCircle(d);
}

function drawBezier3(a, b, c, d) {
  let prev = a;

  const n = 100;
  for (let ii = 0; ii <= n; ii++) {
    const t = ii / n;
    const e = lerp2d(a, b, t);
    const f = lerp2d(b, c, t);
    const g = lerp2d(c, d, t);
    const h = lerp2d(e, f, t);
    const i = lerp2d(f, g, t);
    const j = lerp2d(h, i, t);
    line(prev.x, prev.y, j.x, j.y);

    prev = j;
  }
}

function drawCircle(pos) {
  push();
  strokeWeight(2);
  fill("#292a33");
  circle(pos.x, pos.y, 10);
  pop();
}

function lerp2d(a, b, t) {
  return { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t) };
}

点の数が4つの3次ベジェ曲線の作例。

ベジェ曲線の別の描き方

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

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

  • 点AB間の補間係数 t による線形補間の式は A \times (1 - t) + B \times t
  • 点BC間の補間係数 t による線形補間の式は B \times (1 - t) + C \times t

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

(A \times (1 - t) + B \times t)(1 - t) + (B \times (1 - t) + C \times t) \times t

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

function setup() {
  createCanvas(windowWidth, windowHeight);
  noFill();
  stroke(240);

  const space = 20;
  const a = { x: space, y: height - space };
  const b = { x: width / 2, y: space };
  const c = { x: width - space, y: height - space };

  drawBezier(a, b, c);

  drawCircle(a);
  drawCircle(b);
  drawCircle(c);
}

function drawBezier(a, b, c) {
  let prev = a;

  const n = 100;
  for (let i = 0; i <= n; i++) {
    const t = i / n;
    const dx = calcBezier(a.x, b.x, c.x, t);
    const dy = calcBezier(a.y, b.y, c.y, t);
    line(prev.x, prev.y, dx, dy);

    prev = { x: dx, y: dy };
  }
}

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

function drawCircle(pos) {
  push();
  strokeWeight(2);
  fill("#292a33");
  circle(pos.x, pos.y, 10);
  pop();
}

上記で求めた式を利用した2次ベジェ曲線を描く作例。

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

(x+y)^n 展開した式
(x+y)^1 x+y
(x+y)^2 x^2+2xy+y^2
(x+y)^3 x^3+3x^2y+3xy^2+y^3
(x+y)^4 x^4+4x^3y+6x^2y^2+4xy^3+y^4

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

次数 x(1-t)yt に置き換えた場合
1次 (1-t)+t
2次 (1-t)^2+2 \cdot (1-t) \cdot t+t^2
3次 (1-t)^3+3 \cdot (1-t)^2 \cdot t +3 \cdot (1-t) \cdot t^2 + t^3
4次 (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を渡して3次ベジェ曲線の位置を求める場合は、下記の式になる。

A \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 setup() {
  createCanvas(windowWidth, windowHeight);
  noFill();
  stroke(240);

  const space = 20;
  const a = { x: space, y: height - space };
  const b = { x: width / 3, y: space };
  const c = { x: (width / 3) * 2, y: height - space };
  const d = { x: width - space, y: space };

  drawBezier(a, b, c, d);

  drawCircle(a);
  drawCircle(b);
  drawCircle(c);
  drawCircle(d);
}

function drawBezier(a, b, c, d) {
  let prev = a;

  const n = 100;
  for (let i = 0; i <= n; i++) {
    const t = i / n;
    const dx = calcBezier3(a.x, b.x, c.x, d.x, t);
    const dy = calcBezier3(a.y, b.y, c.y, d.y, t);
    line(prev.x, prev.y, dx, dy);

    prev = { x: dx, y: dy };
  }
}

function calcBezier3(a, b, c, d, t) {
  return a * (1 - t) ** 3 + b * 3 * (1 - t) ** 2 * t + c * 3 * (1 - t) * t ** 2 + d * t ** 3;
}

function drawCircle(pos) {
  push();
  strokeWeight(2);
  fill("#292a33");
  circle(pos.x, pos.y, 10);
  pop();
}

p5.jsで利用できるベジェ曲線の関数

function setup() {
  createCanvas(windowWidth, windowHeight);
  stroke(240);
  noFill();

  bezier(0, height, width / 3, 0, (width / 3) * 2, height, width, 0);
}
bezier(x1, y1, x2, y2, x3, y3, x4, y4)

(x1, y1) が始点、(x2, y2), (x3, y3) が制御点、(x4, y4) が終点の3次ベジェ曲線を描く。

let pos, t;

function setup() {
  createCanvas(windowWidth, windowHeight);
  stroke(240);
  noFill();

  pos = [
    { x: 0, y: height },
    { x: width / 3, y: 0 },
    { x: (width / 3) * 2, y: height },
    { x: width, y: 0 },
  ];

  t = 0;
}

function draw() {
  clear();
  bezier(pos[0].x, pos[0].y, pos[1].x, pos[1].y, pos[2].x, pos[2].y, pos[3].x, pos[3].y);

  const x = bezierPoint(pos[0].x, pos[1].x, pos[2].x, pos[3].x, t);
  const y = bezierPoint(pos[0].y, pos[1].y, pos[2].y, pos[3].y, t);
  fill("#292a33");
  circle(x, y, 15);

  t += 0.002;
  t %= 1;
}
bezierPoint(a, b, c, d, t)

a が始点、b, c が制御点、d が終点の3次ベジェ曲線の計算をして、補間係数が t のときの位置を返す。a, b, c, d は軸ごとの値を渡す必要がある。