JavaScript「10:線分とベジェ曲線の数式を最適化する」

直線から曲線に至る数式の並びには、とある規則性のようなものが見受けられました。 知っておくと、プログラムで単に直線を扱うのだけでも役に立つかもしれません。

1次ベジェ曲線(線分)のクラス定義


class Line extends Collider { //当たり判定を線で扱うクラス 背景オブジェクトの通行判定に使う予定
  constructor(x, y, x1, y1) {
    super('line', x, y);
    this.x1 = x1; //終着点のx座標
    this.y1 = y1; //終着点のy座標
  }

  /* (1次曲線と捉えた)直線の数式(0 <= t <=1) t地点の座標を求める*/
  fx(t) { return (this.x1 - this.x)*t + this.x;}
  fy(t) { return (this.y1 - this.x)*t + this.y;}
  
  /* 微分式、直線の傾きを求める
  f1x(t) { return this.x1 - this.x;}
  f1y(t) { return this.y1 - this.y;}
  */
  get dx() { return this.x1 - this.x; } // x軸の移動方向と距離を求める、ベクトル成分のxになる
  get dy() { return this.y1 - this.y; } // y軸の移動方向と距離を求める、ベクトル成分のyになる
}

これは線分(1次曲線=直線)のクラスを定義したものです。
2次や3次の曲線のように、0〜1で変化する定数tを用いて座標を求める式を描いてます。

1次ベジェ曲線(直線)の数式

/* (1次曲線と捉えた)線分の数式(0 <= t <=1) t地点の座標を求める*/
  fx(t) { return (this.x1 - this.x)*t + this.x;}
  fy(t) { return (this.y1 - this.x)*t + this.y;}

この式は使わないけど、とりあえず。

1次ベジェ曲線(直線)の傾きを求める微分式

  get dx() { return this.x1 - this.x; } // x軸の移動方向と距離を求める、ベクトル成分のxになる
  get dy() { return this.y1 - this.y; } // y軸の移動方向と距離を求める、ベクトル成分のyになる

1次のtの係数は、xの傾きとしてdx、yの傾きとしてdyとしています。
すると、1次曲線の元の式はこのように表されます。
/* 線分の数式(0 <= t <=1) */
  fx(t) { return this.dx*t + this.x;}
  fy(t) { return this.dy*t + this.y;}

ありきたりでふーーん。。(' '*) 何の意味もない解説です。 で、次に2次曲線について見ていきますと、どうでしょう。。

2次ベジェ曲線のクラス定義


class QuadraticCurve extends Collider { //当たり判定を2次曲線で扱うクラス、物体の放物線の軌道だったり力場を作ったりする。
  constructor(x, y, x1, y1, x2, y2) {
    super('quadraticCurve', x, y);
    this.x1 = x1; //制御点1のx座標
    this.y1 = y1; //制御点1のy座標
    this.x2 = x2; //終着点のx座標
    this.y2 = y2; //終着点のy座標
  }
  // 曲線上のX座標 = fx(t) =  (this.x2 - 2*this.x1 + this.x)*t*t + 2*(this.x1 - this.x)*t + this.x;
  // 曲線上のY座標 = fy(t) =  (this.y2 - 2*this.y1 + this.y)*t*t + 2*(this.y1 - this.y)*t + this.y;

  get d2x() { return this.x2 - 2*this.x1 + this.x; } // 曲線上のX座標を求める数式のt**2の係数
  get dx() { return this.x1 - this.x; } // 曲線上のX座標を求める数式のt**1の係数の半分...使う時は2*dxとして使う。
  get d2y() { return this.y2 - 2*this.y1 + this.y; } // 曲線上のY座標を求める数式のt**2の係数
  get dy() { return this.y1 - this.y; } // 曲線上のY座標を求める数式のt**1の係数の1/2...使う時は2*dyとして使う。

  /* 2次ベジェ曲線の数式(0 <= t <=1) t地点の座標を求める*/
  fx(t) { return this.d2x*t*t + 2*this.dx*t + this.x; }
  fy(t) { return this.d2y*t*t + 2*this.dy*t + this.y; }

  /* 2次曲線の微分式(0 <= t <=1) の時の、曲線の傾きを求める */
  f1x(t) { return 2*(this.d2x*t + this.dx); }
  f1y(t) { return 2*(this.d2y*t + this.dy); }
}

1次曲線(直線)は、(x,y)と(x1,y1)という2つの座標点で表されましたが、2次曲線ではさらに(x2,y2)という3つ目の座標点が追加されます。 2次曲線上の座標を求める数式を、0〜1の間で変化する定数tを用いて表すとどうでしょう。

2次ベジェ曲線の数式

  fx(t) =  (this.x2 - 2*this.x1 + this.x)*t*t + 2*(this.x1 - this.x)*t + this.x;
  fy(t) =  (this.y2 - 2*this.y1 + this.y)*t*t + 2*(this.y1 - this.y)*t + this.y;
ふむ、ちょっと複雑になりました。 では、ここでtの1次の係数と、2次の係数とを定数に置き換えてみます。 ここでのポイントは、1次曲線のときに定義したdx=(this.x1 - this.x)やdy=(this.y1 - this.y)を此処でも活用することです。

2次ベジェ曲線のtの係数を置き換える

  get d2x() { return this.x2 - 2*this.x1 + this.x; } // 曲線上のX座標を求める数式のt**2の係数
  get dx() { return this.x1 - this.x; } // 曲線上のX座標を求める数式のt**1の係数の半分...使う時は2*dxとして使う。
  get d2y() { return this.y2 - 2*this.y1 + this.y; } // 曲線上のY座標を求める数式のt**2の係数
  get dy() { return this.y1 - this.y; } // 曲線上のY座標を求める数式のt**1の係数の1/2...使う時は2*dyとして使う。

1次曲線で定義した定数dx,dyをそのまま使い、2次のtの係数をd2x,d2yにまとめると、2次曲線の関数は以下のようになります。

2次ベジェ曲線の数式Ⅱ

  /* 2次ベジェ曲線の数式(0 <= t <=1) t地点の座標を求める*/
  fx(t) { return this.d2x*t*t + 2*this.dx*t + this.x; }
  fy(t) { return this.d2y*t*t + 2*this.dy*t + this.y; }

2次関数の、1次の係数が2の倍数で表せるようになりました。
このことは、2次方程式の解の公式を簡略化できる大きなメリットになります。

2次曲線の当たり判定(後日追記分)

2次ベジェ曲線の傾きを求める微分式

  /* 2次曲線の微分式(0 <= t <=1) の時の、曲線の傾きを求める */
  f1x(t) { return 2*(this.d2x*t + this.dx); }
  f1y(t) { return 2*(this.d2y*t + this.dy); }

1次式のdx,dyを流用すると、微分式でも面白い発見ができます。 なんと、2次曲線を微分した式から元の1次曲線(直線)の式が形を変えて出てきているのです。 元の1次曲線(直線)の式と、2次曲線の微分式を比べてみると、相違点がいい感じに見えてきます。

元の1次曲線(直線)の式と、2次曲線の微分式

  /* 1次曲線(直線)(0 <= t <=1) の座標を求める式 */
  fx(t) { return this.dx*t + this.x;}
  fy(t) { return this.dy*t + this.y;}

  /* 2次曲線の微分式(0 <= t <=1) の時の、曲線の傾きを求める */
  f1x(t) { return 2*(this.d2x*t + this.dx); }
  f1y(t) { return 2*(this.d2y*t + this.dy); }

ちょうど、微分式が元の直線の式をベースにしてることが伺えます。 極値を求めるときなどは微分式=0で計算するため、その際に外側の*2は無視できるメリットもある。非常に計算しやすい形になります。

ここまででも面白い発見ですが、さらに3次曲線の数式まで広げてみるとどうでしょう。

3次ベジェ曲線のクラス定義


class BezierCurve extends Collider { //当たり判定を3次曲線で扱うクラス。地形を現す曲線なのでマップに固定。通行判定に使う予定。
  constructor(x, y, x1, y1, x2, y2, x3, y3) {
    super('bezierCurve', x, y);
    this.x1 = x1; //制御点1のx座標
    this.y1 = y1; //制御点1のy座標
    this.x2 = x2; //制御点2のx座標
    this.y2 = y2; //制御点2のy座標
    this.x3 = x3; //終着点のx座標
    this.y3 = y3; //終着点のy座標

    this.d3x = this.x3 - this.x + 3*(this.x1 - this.x2); // fx(t) t**3の係数
    this.d2x = this.x2 - 2*this.x1 + this.x; // fx(t) t**2の係数の1/3...使う時は3*d2xとして使う。
    this.dx = this.x1 - this.x; // fx(t) t**1の係数の1/3...使う時は3*dxとして使う。

    this.d3y = this.y3 - this.y + 3*(this.y1 - this.y2); // fy(t) t**3の係数
    this.d2y = this.y2 - 2*this.y1 + this.y; // fy(t) t**2の係数の1/3...使う時は3*d2yとして使う。
    this.dy = this.y1 - this.y; // fy(t) t**1の係数の1/3...使う時は3*dyとして使う。
  }

  /* 3次ベジェ曲線の数式(0 <= t <=1) t地点の座標を求める*/
  fx(t) { return this.d3x*t*t*t + 3*this.d2x*t*t + 3*this.dx*t + this.x; } //tを代入してX座標取得。
  fy(t) { return this.d3y*t*t*t + 3*this.d2y*t*t + 3*this.dy*t + this.y; } //tを代入してY座標取得。

  /*3次ベジェ曲線の微分式(0 <= t <=1) t地点の傾きを調べる*/
  f1x(t) { return 3*(this.d3x*t*t + 2*this.d2x*t + this.dx);}
  f1y(t) { return 3*(this.d3y*t*t + 2*this.d2y*t + this.dy);}
}
3次曲線ではさらに4つ目の座標点(x3,y3)が加わります。

3次ベジェ曲線のtの係数を置き換える

    this.d3x = this.x3 - this.x + 3*(this.x1 - this.x2); // fx(t) t**3の係数
    this.d2x = this.x2 - 2*this.x1 + this.x; // fx(t) t**2の係数の1/3...使う時は3*d2xとして使う。
    this.dx = this.x1 - this.x; // fx(t) t**1の係数の1/3...使う時は3*dxとして使う。

    this.d3y = this.y3 - this.y + 3*(this.y1 - this.y2); // fy(t) t**3の係数
    this.d2y = this.y2 - 2*this.y1 + this.y; // fy(t) t**2の係数の1/3...使う時は3*d2yとして使う。
    this.dy = this.y1 - this.y; // fy(t) t**1の係数の1/3...使う時は3*dyとして使う。
1次直線で定義したdx,dy...2次曲線で定義したd2x,d2yを流用し、さらに3次のtの係数を定義して、3次曲線の式を表してみるとどうでしょう。

3次ベジェ曲線の数式

 /* 3次ベジェ曲線の式(0 <= t <=1) t地点の座標を求める*/
  fx(t) { return this.d3x*t*t*t + 3*this.d2x*t*t + 3*this.dx*t + this.x; } //tを代入してX座標取得。
  fy(t) { return this.d3y*t*t*t + 3*this.d2y*t*t + 3*this.dy*t + this.y; } //tを代入してY座標取得。

すると、tの2次の係数と1次の係数が3の倍数となりました。私は3次方程式を真面目に解かないので分かりませんが、この形にも何かしら恩恵があるかもしれませんね。

3次ベジェ曲線の微分式

  /*3次ベジェ曲線の微分式(0 <= t <=1) t地点の傾きを調べる*/
  f1x(t) { return 3*(this.d3x*t*t + 2*this.d2x*t + this.dx);}
  f1y(t) { return 3*(this.d3y*t*t + 2*this.d2y*t + this.dy);}

微分式を見てみると、2次曲線の数式がほぼそのままの形で姿を表しました。
極値を求める際に、やはり計算手順が簡略化される、理想的な式の形ですね。

ではこの微分式を、もう一度微分するとどうでしょう。
3次ベジェ曲線の2回微分式
  /*3次ベジェ曲線の2回微分式(0 <= t <=1) t地点の変曲点を調べる*/
  f2x(t) { return 6*(this.d3x*t + this.d2x);}
  f2y(t) { return 6*(this.d3y*t + this.d2y);}

今度は見事に直線の式が姿を表します(' '*) これが=0のとき、3次関数の変曲点を求める式となります。
すると、x座標の変曲点となるt = -this.d2x / this.d3x...(yも同様に)というのが簡単に求まってしまいます。うーむ感慨深い。


このように、ベジェ曲線は直線(1次曲線)の数式から始まり、式の要素を引き継いだまま2次、3次と拡張させることで、関数にある一定の法則性と計算のメリットが生まれることが分かりました。



こんな感じで、4次関数いってみますか?

【目次】
  1. JavaScriptでゲーム作り「1:基礎編」
  2. JavaScriptでゲーム作り「2:プレイヤーの歩かせ方」
  3. JavaScriptでゲーム作り「3:プレイヤーの向きに応じてアクションを起こす」
  4. JavaScriptでゲーム作り「4:NPCと会話する」
  5. JavaScriptでゲーム作り「5:当たり判定を最適化する」
  6. JavaScriptでゲーム作り「6:線分の当たり判定を実装する」
  7. JavaScriptでゲーム作り「7:フィールドの背景スクロール」
  8. JavaScriptでゲーム作り「8:プログラムの最適化、高速化、コード整理」
  9. JavaScriptでゲーム作り「9:タッチ・マウスイベント入力」
  10. JavaScriptでゲーム作り「10:法線ベクトルと衝突時のバウンス判定」
  11. JavaScriptでゲーム作り「11:多角形を使った魔法陣を実装する」
  12. JavaScriptでゲーム作り「12:キャッシュの扱い方と計算処理の高速化」
  13. JavaScriptでゲーム作り「13:メッセージウィンドウを実装する」
  14. JavaScriptでゲーム作り「14:イベントの仕組みを理解する」
  15. JavaScriptでゲーム作り「15:メッセージテキスト表示の機能追加」
  16. JavaScriptでゲーム作り「16:会話時の立ち絵&名前表示」


古都さんのフレームワークを元にほぼ最初から作ってます。
たぶん順番に見ないとちんぷんかんぷん(' '*)...

すぺしゃるさんくす

https://sbfl.net/
古都さん
コードの見本をありがとう! とても判りやすく、簡潔なソースコード。いつも勉強になってます。


プレイヤーキャラ
ぴぽやさんもありがとう、この画像作るの、私では大変でした。助かりました。