WebGL
three.js

ICS MEDIAの記事でベクトルの勉強

ベクトル=向きと大きさをもった量

基準点Oから目標点Aまでの方向と距離の量 $\vec{OA}$
これをAの 位置ベクトル と呼ぶ

使用メソッド一覧

関数 内容
.add() 足し算 / A.add(B) = A + B
.sub() 引き算 / A.sub(B) = A - B
.negate() ベクトルの反転
.normalize() 正規化 単位ベクトルに変換
.multiplyScalar() ベクトルの拡縮
.dot() 内積(スカラーを求める)
.cross() 外積(ベクトルを求める)
.up.set() Meshの上向きベクトルを決める

足し算・引き算

https://ics.media/entry/15043

Demo : https://codepen.io/mo4_9/pen/zzyRov

回転する球体の現在の位置(oldPosition)と次のフレームの位置(newPosition)からベクトルを割り出す
正面方向のベクトル:newPosition - oldPosition
背面方向のベクトル:oldPosition - newPosition

ベクトルの引き算
const oldPosition = sphere.position.clone();
const newPosition = getCirculMotionPosition(degree);
// ベクトルを割り出す
frontVector = newPosition.clone().sub(oldPosition);
backVector = frontVector.clone().negate();
// 正規化
frontVector = frontVector.normalize();
backVector = backVector.normalize();

後ろから追従するカメラの位置を割り出すというのは、ベクトルが「原点」から「回転する球体」を経由して「追従するカメラの位置」に伸びるというイメージ(つまり足し算)

ベクトルの足し算
backVector.multiplyScalar(distance); // 一定距離はなす
const cameraPosition = backVector.add(sphere.position);

参考
ベクトルの足し算と引き算

内積

https://ics.media/entry/15321

コードは長いのでgithubでforkした

【公式】
$$
OA・OB = |OA||OB|\cos\theta
$$

OAの長さ=1, OBの長さ=2, 角度=60の場合
1 * 2 * Math.cos(Math.PI / 180 * 60)
の計算の結果、内積は1となる

【公式の変形】
$$
\cos\theta = OA・OB \div |OA||OB|
$$

OA,OBの長さが1の場合、$\cos\theta = OA・OB$ になる
つまり 単位ベクトル(長さが1のベクトル)同士の内積は$\cos\theta$を求める ことになる。

単位ベクトルODと点Cがあった場合、単位ベクトルODから見て点Cの位置は、
$\cos\theta$がプラス値であれば前方、マイナス値であれば後方にあると判別できる。

今回は懐中電灯の正面ベクトル(OD)から見てパーティクルの位置(C)が前後どちらにあるかを判定しているということになる。絞り値が0.2なので照らしている範囲は狭いが、1.0にすれば横に180度広がる。

コードのポイントは、ParticleEmitterクラスに生えているupdate()メソッド

ParticleEmitter.js
update(lightFrontVector, aperture) {
  let target = lightFrontVector.clone();
  // 全てのパーティクルに対して照らされているか判定
  _.each(this._particleStore, (particle) => {
    // 絞り値から透明度の割合を算出
    // 「懐中電灯の正面ベクトル」と「パーティクルの位置ベクトル」の方向が
    // 近ければ近いほどパーティクルの不透明度が1に近づいていく
    let dot = particle.position.clone().normalize().dot(target);
    // 内積は計算結果がベクトルではなく数値になる
    // 表示される範囲が広すぎるため少し狭める 0.8 ~ 1.0
    let opacity = (dot - (1 - aperture)) / aperture;
    // ちらつかせます
    opacity *= Math.random();
    // 透明度を設定
    particle.material.opacity = opacity;
  });
}

用途

  • 目標物が物体の前後にあるかどうかを判別
  • キャラクターの視界に敵がいるかどうかの判定
  • カメラの視界外の物体は描画しないようにする など

参考
【数学】「内積」の意味をグラフィカルに理解すると色々見えてくる その1

外積

https://ics.media/entry/15467

【公式】
$$
OA \times OB = OC
$$

  • ベクトルは垂直に交わる
  • 点Oを軸にOAベクトルをOBベクトルへ移動するようにネジを回した時に動く進む方向が外積の向き
  • 乗算する順番を変えるとベクトルの向きは反転する
  • OAベクトルとOBベクトルの二辺からできる平行四辺形の面積が、そのままOCベクトルの長さになる

法線

法線=面に直角に交わるベクトル(面の向きを表すベクトル)

トロッコの正面ベクトルと、
z向きの単位ベクトルの外積から
コースの法線ベクトルを求める

コースの法線ベクトルを求める
_getNormal(currentPoint, nextPoint) {
  let frontVec = nextPoint.clone().sub(currentPoint).normalize(); // x
  let sideVec = new THREE.Vector3(0, 0, 1); // z
  let normalVec = frontVec.cross(sideVec); // y

  return normalVec;
}

求めた法線でトロッコの上向きベクトルを更新する。
(向きを変える際に頭がどの方向にあれば良いかを決める)
this._truck.upのデフォルト値はVector3 {x: 0, y: 1, z: 0}
ここでの法線ベクトルはy向きなのでnormal.yが毎フレームごと変更される。

// トラックの位置を修正
this._truck.position.copy(this._course.points[this._frame]);
// トロッコの上向きベクトルを更新
this._truck.up.set(normal.x, normal.y, normal.z);
// 正面を向く(上向きベクトルの変更を反映させる)
this._truck.lookAt(this._course.points[this._frame + 1]);

なぜ更新するかというと、例えばトロッコがコースの下部を走っているときは、normal.yがマイナス方向に振れて逆さまになっていないとおかしい。デフォルトだとずっとy: 1なので、挙動がおかしくなるというわけ。

用途

  • 衝突判定
  • 光の表現 など

参考
内積と外積の使い方