Chapter 19

ベクトル

miku
miku
2021.11.12に更新

ベクトル

向きと長さが同じである2つの矢印がある。これらは位置は違うが、向きと長さが同じなので、重ねればぴったりと合うことになる。

このように長さと向きだけを考えたものをベクトルといい、2つの矢印はまったく同じベクトルであるといえる。

ベクトルの長さは距離以外の用途にも利用されるので、長さのことを代わりに大きさと呼ぶ。なので、ベクトルは向きと大きさを持つ情報と定義される。

ベクトルにおいて位置は関係がないということは、ベクトルの始点をすべて原点に合わせることにすると、ベクトルの終点の座標(x, y)さえ持っていればベクトルを表すことができる。このような軸ごとの値(x, y)のことをベクトルの成分と呼ぶ。

後述するが、ベクトルの成分さえあれば大きさや向きを取得できる。

次からはコードでベクトルを扱う方法について学ぶ。p5.jsにはベクトルを扱う関数が一通り用意されているが、まずは自前で作成するところから解説する。

ベクトルの作成

function create(x, y) {
  return { x, y };
}

ベクトルは複数作られることを考えると、ベクトルの成分はオブジェクトとしてまとまっていたほうが都合がいい。なので、create()(x, y) を渡すと、{ x, y } を返すようにする。

ベクトルのクローン

function copy(v) {
  return { x: v.x, y: v.y };
}

copy() にベクトルを渡すと、成分が同じである新しいベクトルを返す。

const a = create(2, 10);
const b = copy(a);

console.log(a === a); // true
console.log(a === b); // false

この関数は、この後に出てくるベクトルの計算において、上書きを行わない場合に利用できる。

ベクトルの四則演算

ベクトルの定義上、加算・減算はベクトル同士で、乗算はベクトルと実数で演算が可能で、除算は定義されていない。しかし、これから実装するベクトルの四則演算では全てベクトル同士・実数で演算可能にする。

理由としてはまず、各成分に実数を足したり割ったりする機能があると計算上便利であるからだ。他にも、ベクトルに入れる値は必ずしもベクトルとは限らないということもある。

たとえば座標やなにかのサイズをオブジェクトとして格納したい場合で適当な型が見当たらない場合はベクトルの成分を利用して格納するケースがある。画面の幅と高さをベクトルに入れておいて、中央の座標を求めたいなという場合は2で成分を割るなどのケースがそうだ。

ベクトル同士の四則演算は各成分ごとに計算を行う。ベクトルと実数の四則演算は各成分と実数の計算になる。詳しくは下記の通りだ。

// a, bがベクトル, cが実数の場合

// 加算
a + b = (a.x + b.x, a.y + b.y)
a + c = (a.x + c, a.y + c)

// 減算
a - b = (a.x - b.x, a.y - b.y)
a - c = (a.x - c, a.y - c)

// 乗算
a * b = (a.x * b.x, a.y * b.y)
a * c = (a.x * c, a.y * c)

// 除算
a / b = (a.x / b.x, a.y / b.y)
a / c = (a.x / c, a.y / c)

上書きするか/新しいベクトルを返すか

// 上書きする場合
function add(a, b) {
  a.x += b.x;
  a.y += b.y;

  return a;
}
// 新しいベクトルを返す場合
function add(a, b) {
  const v = copy(a);
  v.x += b.x;
  v.y += b.y;

  return v;
}

ベクトルの計算を行う場合、引数に渡したベクトルの成分を計算結果で上書きするか、新しいベクトルを作り、そちらに計算結果を入れて返す2パターンの方法があり、後者の書き方はいわゆるイミュータブルな状態になる。

下記のコード例では後者の書き方を採用している。

加算

function add(a, b) {
  const v = copy(a);
  if (typeof b === "number") {
    v.x += b;
    v.y += b;
  } else {
    v.x += b.x;
    v.y += b.y;
  }

  return v;
}
const a = create(7, 3);
const b = create(-2, 2);
const c = add(a, b);
console.log(c); // {x: 5, y: 5}
const d = add(c, 3);
console.log(d); // {x: 8, y: 8}

減算

function subtract(a, b) {
  const v = copy(a);
  if (typeof b === "number") {
    v.x -= b;
    v.y -= b;
  } else {
    v.x -= b.x;
    v.y -= b.y;
  }

  return v;
}
const a = create(7, 3);
const b = create(2, 2);
const c = subtract(a, b);
console.log(c); // {x: 5, y: 1}
const d = subtract(c, 3);
console.log(d); // {x: 2, y: -2}

乗算

function multiply(a, b) {
  const v = copy(a);
  if (typeof b === "number") {
    v.x *= b;
    v.y *= b;
  } else {
    v.x *= b.x;
    v.y *= b.y;
  }

  return v;
}
const a = create(7, 3);
const b = create(2, 2);
const c = multiply(a, b);
console.log(c); // {x: 14, y: 6}
const d = multiply(c, 2);
console.log(d); // {x: 28, y: 12}

除算

function divide(a, b) {
  const v = copy(a);
  if (typeof b === "number") {
    v.x /= b;
    v.y /= b;
  } else {
    v.x /= b.x;
    v.y /= b.y;
  }

  return v;
}
const a = create(100, 100);
const b = create(4, 2);
const c = divide(a, b);
console.log(c); // {x: 25, y: 50}
const d = divide(c, 5);
console.log(d); // {x: 5, y: 10}

ベクトルの大きさ

ベクトルのx成分、y成分の辺の間の角が直角になるので、三平方の定理を使用すれば、残りの辺に対応するベクトルの大きさを求めることができる。

function length(v) {
  return sqrt(v.x * v.x + v.y * v.y);
}
const v = create(5, 5);
console.log(length(v)); // 7.0710678118654755

ベクトルの正規化 / 単位ベクトル

ベクトルの大きさが 1 になるように、ベクトルの成分を調整することをベクトルの正規化と呼び、この大きさ 1 のベクトルを単位ベクトルと呼ぶ。大きさ 1 のベクトルの成分は、必ず -1~1 の範囲に収まる。

function normalize(v) {
  return multiply(v, 1 / length(v));
}
const a = create(6, 6);
console.log(length(a)); // 8.48528137423857
console.log(length(normalize(a))); // 1

正規化の方法は、1 / ベクトルの大きさ を各成分に掛ければいい。 ベクトルの向きはそのままで大きさだけを変えるので、大きさの変更前と変更後のベクトルと各成分でできる直角三角形は相似の関係になる。相似の関係は3組の辺の比が同じなので、大きさを 1 にするために掛けた数を、各成分に掛ける必要があるからだ。divide(v, length(v)) にしても同じ結果が得られる。

const a = create(6, 6); // 向き45度、大きさ8.4852...のベクトル
const b = create(0, 3); // 向き90度、大きさ3のベクトル

normalize(a); // 正規化することで、向き45度、大きさ1のベクトルになる。
normalize(b); // 正規化することで、向き90度、大きさ1のベクトルになる。

ベクトルは大きさと向きを持つ量だが、正規化することで大きさが 1 になり、向きだけを持つベクトルと考えることができる。

ベクトルの大きさの設定

ベクトルの大きさが任意の値になるように各成分を調整する。

function setMag(v, len) {
  const nv = normalize(v);
  nv.x *= len;
  nv.y *= len;

  return nv;
}
const a = create(6, 6);
console.log(length(a)); // 8.48528137423857
console.log(length(setMag(a, 4))); // 4

一度正規化した後、設定したい大きさを各成分に掛け合わせる。正規化されたベクトルの大きさは 1 で、そこから設定した大きさを掛けるのだから、相似の三角形を維持するには各成分に設定する値を掛ける必要があるからだ。

ベクトルの大きさの制限

function limit(v, len) {
  const minLen = min(len, length(v));
  return setMag(v, minLen);
}
const a = create(10, 10);
console.log(length(a)); // 14.142135623730951
console.log(length(limit(a, 10))); // 10
console.log(length(limit(a, 20))); // 14.142135623730951

limit(v, len) は、ベクトル v の大きさが len より大きいなら len に、でなければもとの大きさのベクトルを返す関数だ。

ベクトル v の大きさと len の小さい方を新たなベクトルの大きさにすればいい。

ベクトルの向き(角度)

function heading(v) {
  return atan2(v.y, v.x);
}
const v = create(5, 5);
console.log(heading(v)); // 0.7853981633974483

heading(v) は、x軸とベクトル v のなす角度をラジアン単位で返す。

内積

function dot(a, b) {
  return a.x * b.x + a.y * b.y;
}

2 つのベクトル a, b があり、aの大きさ * bの大きさ * cosθ あるいは a.x * b.x + a.y * b.y のように各成分同士を掛けて足し合わせたものを内積と呼び、前者と後者の値は一致する。

前者の cosθθ は、2つのベクトルの始点を原点に置いたときのベクトル間の角度、いわゆるなす角になる。

// 2つのベクトルを用意する
const a = create(100, 0); // 3時の向きを指すベクトル
const b = create(0, 10000); // 6時の向きを指すベクトル

const d = dot(a, b); // 内積を計算
const cos = d / (length(a) * length(b)); // aの大きさ*bの大きさで割ることでcosθを取得
const rad = acos(cos); // cosθから逆三角関数でθを取得
const deg = (rad * 180) / PI; // ラジアンから角度に変換

console.log(deg); // 90;

前者の式である aの大きさ * bの大きさ * cosθaの大きさ * bの大きさ で割ると cosθ になるので、後者の式から aの大きさ * bの大きさ を割ると同じく cosθ だけが残る。

// 2つのベクトルを用意する
let a = create(100, 0); // 3時の向きを指すベクトル
let b = create(0, 10000); // 6時の向きを指すベクトル

a = normalize(a); // 大きさが1になるように正規化する
b = normalize(b); // 大きさが1になるように正規化する

const cos = dot(a, b); // 2つのベクトルが正規化済みなので、内積の結果がcosθになる
const rad = acos(cos); // cosθから逆三角関数でθを取得
const deg = (rad * 180) / PI; // ラジアンから角度に変換

console.log(deg); // 90;

2つのベクトルを前もって正規化しておけば、内積の結果が 1 \cdot 1 \cdot cos\theta と直接 cosθ になる。

ここまでで自前でベクトルを実装する方法について扱った。次にp5.jsで定義されているベクトルの関数について解説する。

p5.jsで定義されているベクトルの関数

const v = createVector(10, 20, 30);
console.log(v.x, v.y, v.z); // 10 20 30

createVector(x, y, z) で成分が (x, y, z) のベクトルを作成する。引数を指定しなかった成分は 0 になる。各成分は x, y, z から参照できる。

const a = createVector(10, 20);
const b = createVector(20, 30);

// 加算
a.add(b); // 各成分ごとに加算する(aに上書き)
a.add(4); // 各成分に引数の値を加算する(aに上書き)
p5.Vector.add(a, b); // 上書きせず結果のベクトルを戻り値として返す

// 減算
a.sub(b); // 各成分ごとに減算する(aに上書き)
a.sub(4); // 各成分に引数の値を減算する(aに上書き)
p5.Vector.sub(a, b); // 上書きせず結果のベクトルを戻り値として返す

// 乗算
a.mult(b); // 各成分ごとに乗算する(aに上書き)
a.mult(4); // 各成分に引数の値を乗算する(aに上書き)
p5.Vector.mult(a, b); // 上書きせず結果のベクトルを戻り値として返す

// 除算
a.div(b); // 各成分ごとに除算する(aに上書き)
a.div(4); // 各成分に引数の値を除算する(aに上書き)
p5.Vector.div(a, b); // 上書きせず結果のベクトルを戻り値として返す

ベクトルの四則演算は add(), sub(), mult(), div() により行い、インスタンスメソッドの関数は上書き、クラスメソッドの関数は上書きせず、計算結果を戻り値として返す。

const a = createVector(10, 20);
const b = createVector(2, 5);
a.div(b); // (a.x / b.x, a.y / b.y, a.z / b.z)

除算には注意をする必要があり、成分を指定していなければ 0 / 0 の0除算になる。上記コード例では createVector() 時にz成分を指定していないので z = 0 になっており、a.div(b)a.z / b.z の計算が行われ0除算になる。

v.copy() // クローンを作る
v.mag() // ベクトルの大きさ
v.magSq() // ベクトルの大きさの二乗 (x*x + y*y + z*z)
v1.dot(v2) // v1とv2の内積
v1.dist(v2) // v1からv2までの二点間距離
v.normalize() // vを正規化する(上書き)
v.setMag(len) // vの大きさがlenになるように成分を調整する
v.limit(len) // vの大きさがlenより大きいならlenになるように成分を調整する
v.heading() // x軸とベクトルvのなす角度をラジアン単位で返す。

p5.jsの代表的なベクトルの関数は上記の通り。