📐

WebGL入門 ~ three.js ③~

2022/06/24に公開

今回は数学のおはなし📐
3D開発に必要な数学の知識 + three.jsでの書き方になります。
数学とは言っても、「角度」「回転」の表現の話なので、イメージ掴めたら多分OKです。(自己判断甘め🍭)

前回の回転の話ともつながるところがあるので、今までの記事のリンクも貼っておきます。
WebGL入門 ~ three.js ①~
WebGL入門 ~ three.js ②~


ラジアン

角度のはなし。

  • 度数法 (1°とか360°とか)
  • 弧度法 (ラジアン、2πr とか)

プログラミング言語の多くでは、弧度法をつかっていきます。

360° = 2π

Javascriptでは Math.PIでπが得られる
sin(角度)とかcos(角度) に入る引数はラジアンを受け取る

ラジアンへの変換

radian = degree / 360 * 2π = degree * Math.PI / 180

ベクトル

ベクトル=「 向きと大きさを持った量
パッと聞くとよくわからないのですが、ベクトルをみてみるとこんな感じ。

  • スカラー=単体の値 (大きさ・位置など1軸の値)
  • ベクトル=複数の値を組み合わせて定義する
    • XYなら二次元ベクトル、XYZで三次元ベクトル

単位ベクトル

「向きだけに注目したい場合」は単位ベクトルを使う
単位ベクトルとは、長さが1のベクトル
どんなベクトルでも、単位ベクトルに変換することができる(長さを1にする)

実際どういうときに使うのか?

今回スクール内では、地球の周りを月が円運動するサンプルを元に試していきました。

地球の周りを月が円運動しているサンプル

時間を取得する場合

Date.now() // ミリ秒単位

three.jsでは THREE.Clock がある
https://threejs.org/docs/#api/en/core/Clock

// インスタンスの生成
const clock = new THREEE.Clock();

// 経過時間を取得
const nowTime = clock.getElapsedTime();

インスタンスの生成時から時間がカウントされる
renderの中などで nowTime を取得して、sin,cosにいれることで円運動する

例えば、Y軸回転させたいとき🪐

const time = this.clock.getElapsedTime();

const sin = Math.sin(time);
const cos = Math.cos(time);

this.moon.position.set(
  cos * App3.MOON_DISTANCE,
  0.0,
  sin * App3.MOON_DISTANCE,
);

Y軸回転の時、x,zに値を入れている。x,y,zを変えると、回転軸がかわる
また、sinとcosを入れ替えると回転の向きが変わる
App3.MOON_DISTANCE はどのくらい離れてるか、円運動の半径分かける。速度も変わる

サイン、コサインは、単位ベクトル


スクリーン空間と3D空間

マウスの位置を取得する

Javascriptでは、mousemovepointermove イベントを設定することで検出できる

window.addEventListener('mousemove', (event) => {
  const mouseX = event.clientX;
  const mouseY = event.clientY;

MouseEvent.clientX MouseEvent.clientY が座標 (上でいくと、mouseX, mouseY)

スクリーン空間(マウスのほう)は、左上が原点の座標系。単位は px

なので
3D空間の座標と単位に変換してから使う
3D空間は中心が原点で、-1.0~1.0 の値なので、それに合わせる

  1. clientX / innerWidth でマウスカーソルのxの値(px) を 0.0~1.0 に変換
    clientY / innerHeight (同上)
  2. その結果を 2倍して、1ひく。→ -1.0 ~ 1.0 の値に変換
  3. Y軸が反転しているので、必要に応じて反転してつかう。
  const scaleX = event.clientX / window.innerWidth * 2.0 - 1.0;
  const scaleY = event.clientX / window.innerHeight * 2.0 - 1.0;

これを使うと、マウス位置にオブジェクトをだしたりできる(マウスについてくる感じ🐥🐥🐥)

マウスの位置を単位化する

マウス位置のままではなく、「マウス位置に応じて円上を動かす」などしたい場合。
ここで単位化が使い所!!
単位ベクトルと組み合わせることで、向きはマウス座標 + 長さは単位化されているので一定の動きがつくれる

three.jsではnormalizeで単位化できます

const vector = new THREE.Vector2(
  scaleX,
  scaleY,
);
vector.normalize();

ベクトルの始点と終点

いままででてきたベクトルは (2, 3) とか。
暗黙の了解で、始点である原点(0, 0) から、終点 (2, 3) のベクトル、ということ。
※座標(2, 3)とベクトル(2, 3) は意味が異なるので注意

始点は任意で定義できるので、地点Aから、地点Bのベクトル、という場合は
[終点x - 始点x , 終点y - 始点y]
で求められる。

three.jsで書く場合、 THREE.Vector3().subVectors をつかってベクトルの差を求められる

this.satellite から this.moon までのベクトル

const subVector = new THREE.Vector3().subVectors(this.moon.position, this.satellite.position);

ベクトル同士を足す・引く

マウス追従などで追従を滑らかにするために、1つの動きに2つのベクトルを足し合わせたりする。
進行方向のベクトル + 位置関係を表すベクトル = 進行方向が少しずれたベクトル
→ ずれたベクトルは大きさも変わるので、単位化して使う。


クォータニオン(四元数)

ベクトルの掛け算 積の求め方「内積」と「外積」がある。
ちなみに割り算はない。

内積

(単位化された)ベクトル同士のなす角の角度を求めたりするのに使われます。
内積はcosθに一致する。のでθ(角度)が求まる。

dot(v) {
	this.x * v.x + this.y * v.y + this.z * v.z
}

かけて足している…何次元になっても同じ。

外積

二次元ベクトルと三次元ベクトルで結果が若干異なる。

  • 二次元ベクトルの外積 → スカラーの値
  • 三次元ベクトルの外積 → 三次元ベクトル (AとBに直行する新しいベクトルCが求まる)

(基本的に三次元ベクトルでしか成り立たないので、二次元の場合はzの値を0にしている状態)

// 二次元
cross( v ) {
	return this.x * v.y - this.y * v.x;
}

// 三次元
crossVectors( a, b ) {
	const ax = a.x, ay = a.y, az = a.z;
	const bx = b.x, by = b.y, bz = b.z;

	this.x = ay * bz - az * by;
	this.y = az * bx - ax * bz;
	this.z = ax * by - ay * bx;

	return this;
}

クォータニオン

今までの姿勢(回転・向き)は、x軸回転、y軸回転・・・というような「オイラー角」による角度表現でした。
これだと、特定の姿勢を表現するのに複雑な計算が必要。
(単純な回転やxyzのどれか1軸に対してのとかならいいけど、よくわからん向きの回転などをする時難しい)

そこで使えるのがクォータニオン

  • XYZであらわした軸ベクトル + 回転量 の 4つの値を配列にもつ
    q = [x, y, z, 回転量];
  • あらゆる姿勢を表現できる
  • 配列の長さ4で表現できる

さて、これらの「内積」と「外積」と「クォータニオン」がどう繋がるかというと
軸ベクトル + 回転量をもとめてクォータニオンにいれると、姿勢の変化量がだせる
AからBに姿勢を変える時、

  • 軸ベクトル = 2つのベクトルの外積で得られるベクトルが軸になる(C)
  • 回転量 = 2つのベクトルの内積から、角度をもとめる(D)
// (C) 変換前と変換後の2つのベクトルから外積で接線ベクトルを求める
const tangent = new THREE.Vector3().crossVectors(previousDirection, *this*.satelliteDirection);
tangent.normalize();

// (D) 変換前と変換後のふたつのベクトルから内積でコサインを取り出す
const cos = previousDirection.dot(*this*.satelliteDirection);
// (D) コサインをラジアンに戻す
const radians = Math.acos(cos);

// 求めた接線ベクトルとラジアンからクォータニオンを定義
const qtn = new THREE.Quaternion().setFromAxisAngle(tangent, radians);

// 人工衛星の現在のクォータニオンに乗算する
this.satellite.quaternion.premultiply(qtn);

crossVectors 外積
dot 内積
acos cosθのθ(ラジアン)をかえす
THREE.Quaternion().setFromAxisAngle で新たに、回転を加えるためのクォータニオンを作成
this.satellite.quaternion で回転させるオブジェクトのクォータニオンを取得して、作った回転を乗算する

※すべての3Dオブジェクトが、クォータニオンプロパティを持っている。

かんそう

数学わかったぞ〜と思いつつ、これを生かした表現ができるのかな〜自分〜〜と思っています。笑
マウスに追従する動きとかは、お仕事でも取り入れていきやすそう。

Discussion