src/math/S3Vector.js
import S3Math from "./S3Math.js";
import S3Matrix from "./S3Matrix.js";
/**
* 3DCG用のベクトルクラス(immutable)
*
* @class
* @module S3
*/
export default class S3Vector {
/**
* ベクトルを作成します。
* @param {number} x x成分
* @param {number} y y成分
* @param {number} [z=0.0] z成分
* @param {number} [w=1.0] w成分(遠近除算用)
*/
constructor(x, y, z, w) {
/**
* x成分
* @type {number}
*/
this.x = x;
/**
* y成分
* @type {number}
*/
this.y = y;
if (z === undefined) {
/**
* z成分
* @type {number}
* @default 0.0
*/
this.z = 0.0;
} else {
this.z = z;
}
if (w === undefined) {
/**
* w成分(遠近除算用)
* @type {number}
* @default 1.0
*/
this.w = 1.0;
} else {
this.w = w;
}
}
/**
* 自身のクローンを作成します。
* @returns {S3Vector} 複製されたベクトル
*/
clone() {
return new S3Vector(this.x, this.y, this.z, this.w);
}
/**
* 各成分を反転したベクトルを返します。
* @returns {S3Vector}
*/
negate() {
return new S3Vector(-this.x, -this.y, -this.z, this.w);
}
/**
* 2つのベクトルの外積を計算します。
* @param {S3Vector} tgt
* @returns {S3Vector} 外積ベクトル
*/
cross(tgt) {
return new S3Vector(
this.y * tgt.z - this.z * tgt.y,
this.z * tgt.x - this.x * tgt.z,
this.x * tgt.y - this.y * tgt.x,
1.0
);
}
/**
* 2つのベクトルの内積を計算します。
* @param {S3Vector} tgt
* @returns {number} 内積値
*/
dot(tgt) {
return this.x * tgt.x + this.y * tgt.y + this.z * tgt.z;
}
/**
* ベクトル同士の加算を行います。
* @param {S3Vector} tgt
* @returns {S3Vector} 加算結果
*/
add(tgt) {
return new S3Vector(this.x + tgt.x, this.y + tgt.y, this.z + tgt.z, 1.0);
}
/**
* ベクトル同士の減算を行います。
* @param {S3Vector} tgt
* @returns {S3Vector} 減算結果
*/
sub(tgt) {
return new S3Vector(this.x - tgt.x, this.y - tgt.y, this.z - tgt.z, 1.0);
}
/**
* ベクトルの各成分にスカラー、ベクトル、または行列を掛けます。
* @param {number|S3Vector|S3Matrix} tgt
* @returns {S3Vector}
*/
mul(tgt) {
if (tgt instanceof S3Vector) {
return new S3Vector(this.x * tgt.x, this.y * tgt.y, this.z * tgt.z, 1.0);
} else if (tgt instanceof S3Matrix) {
// 横ベクトル×行列=横ベクトル
const v = this;
const A = tgt;
// vA = u なので、各項を行列の列ごとで掛け算する
return new S3Vector(
v.x * A.m00 + v.y * A.m10 + v.z * A.m20 + v.w * A.m30,
v.x * A.m01 + v.y * A.m11 + v.z * A.m21 + v.w * A.m31,
v.x * A.m02 + v.y * A.m12 + v.z * A.m22 + v.w * A.m32,
v.x * A.m03 + v.y * A.m13 + v.z * A.m23 + v.w * A.m33
);
} else if (typeof tgt === "number") {
return new S3Vector(this.x * tgt, this.y * tgt, this.z * tgt, 1.0);
} else {
throw "IllegalArgumentException";
}
}
/**
* x成分のみ変更した新しいベクトルを返します。
* @param {number} x
* @returns {S3Vector}
*/
setX(x) {
return new S3Vector(x, this.y, this.z, this.w);
}
/**
* y成分のみ変更した新しいベクトルを返します。
* @param {number} y
* @returns {S3Vector}
*/
setY(y) {
return new S3Vector(this.x, y, this.z, this.w);
}
/**
* z成分のみ変更した新しいベクトルを返します。
* @param {number} z
* @returns {S3Vector}
*/
setZ(z) {
return new S3Vector(this.x, this.y, z, this.w);
}
/**
* w成分のみ変更した新しいベクトルを返します。
* @param {number} w
* @returns {S3Vector}
*/
setW(w) {
return new S3Vector(this.x, this.y, this.z, w);
}
/**
* 各成分の最大値を持つ新しいベクトルを返します。
* @param {S3Vector} tgt
* @returns {S3Vector}
*/
max(tgt) {
return new S3Vector(
Math.max(this.x, tgt.x),
Math.max(this.y, tgt.y),
Math.max(this.z, tgt.z),
Math.max(this.w, tgt.w)
);
}
/**
* 各成分の最小値を持つ新しいベクトルを返します。
* @param {S3Vector} tgt
* @returns {S3Vector}
*/
min(tgt) {
return new S3Vector(
Math.min(this.x, tgt.x),
Math.min(this.y, tgt.y),
Math.min(this.z, tgt.z),
Math.min(this.w, tgt.w)
);
}
/**
* 各成分が等しいか判定します。
* @param {S3Vector} tgt
* @returns {boolean} 全ての成分が等しい場合true
*/
equals(tgt) {
return (
S3Math.equals(this.x, tgt.x) &&
S3Math.equals(this.y, tgt.y) &&
S3Math.equals(this.z, tgt.z) &&
S3Math.equals(this.w, tgt.w)
);
}
/**
* 2つのベクトル間を線形補間します。
* @param {S3Vector} tgt
* @param {number} alpha 補間係数(0~1)
* @returns {S3Vector}
*/
mix(tgt, alpha) {
return new S3Vector(
S3Math.mix(this.x, tgt.x, alpha),
S3Math.mix(this.y, tgt.y, alpha),
S3Math.mix(this.z, tgt.z, alpha),
S3Math.mix(this.w, tgt.w, alpha)
);
}
/**
* ノルム(二乗和の平方根、長さ)を計算します。
* @returns {number} ベクトルの長さ
*/
norm() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
/**
* ノルムの2乗値(高速、平方根なし)を返します。
* @returns {number} 長さの二乗
*/
normFast() {
return this.x * this.x + this.y * this.y + this.z * this.z;
}
/**
* 正規化した新しいベクトルを返します。
* @returns {S3Vector}
*/
normalize() {
let b = this.normFast();
if (b === 0.0) {
throw "divide error";
}
b = Math.sqrt(1.0 / b);
return new S3Vector(this.x * b, this.y * b, this.z * b, 1.0);
}
/**
* ベクトルを文字列化します。
* @param {number} [num] 成分数指定(省略時は4成分)
* @returns {string}
*/
toString(num) {
if (num === 1) {
return "[" + this.x + "]T";
} else if (num === 2) {
return "[" + this.x + "," + this.y + "]T";
} else if (num === 3) {
return "[" + this.x + "," + this.y + "," + this.z + "]T";
} else {
return "[" + this.x + "," + this.y + "," + this.z + "," + this.w + "]T";
}
}
/**
* ベクトルのハッシュ値を返します。
* @param {number} [num] 成分数指定(省略時は1成分)
* @returns {number}
*/
toHash(num) {
const s = 4;
const t = 10000;
let x = (parseFloat(this.x.toExponential(3).substring(0, 5)) * 321) & 0xffffffff;
if (num >= 2) {
x = (x * 12345 + parseFloat(this.y.toExponential(s).substring(0, s + 2)) * t) & 0xffffffff;
}
if (num >= 3) {
x = (x * 12345 + parseFloat(this.z.toExponential(s).substring(0, s + 2)) * t) & 0xffffffff;
}
if (num >= 4) {
x = (x * 12345 + parseFloat(this.w.toExponential(s).substring(0, s + 2)) * t) & 0xffffffff;
}
return x;
}
/**
* 他の型のインスタンスに変換します(配列化)。
* @param {{new(array: number[]): any}} Instance 配列型のコンストラクタ
* @param {number} dimension 配列長
* @returns {*} 変換結果
*/
toInstanceArray(Instance, dimension) {
if (dimension === 1) {
return new Instance([this.x]);
} else if (dimension === 2) {
return new Instance([this.x, this.y]);
} else if (dimension === 3) {
return new Instance([this.x, this.y, this.z]);
} else {
return new Instance([this.x, this.y, this.z, this.w]);
}
}
/**
* 配列に成分をプッシュします。
* @param {Array<number>} array
* @param {number} num 成分数
*/
pushed(array, num) {
if (num === 1) {
array.push(this.x);
} else if (num === 2) {
array.push(this.x);
array.push(this.y);
} else if (num === 3) {
array.push(this.x);
array.push(this.y);
array.push(this.z);
} else {
array.push(this.x);
array.push(this.y);
array.push(this.z);
array.push(this.w);
}
}
/**
* tgtへの方向ベクトルを取得します。
* @param {S3Vector} tgt
* @returns {S3Vector} tgt-自身のベクトル
*/
getDirection(tgt) {
return tgt.sub(this);
}
/**
* tgtへの正規化された方向ベクトルを取得します。
* @param {S3Vector} tgt
* @returns {S3Vector}
*/
getDirectionNormalized(tgt) {
return tgt.sub(this).normalize();
}
/**
* tgtとの距離を返します。
* @param {S3Vector} tgt
* @returns {number}
*/
getDistance(tgt) {
return this.getDirection(tgt).norm();
}
/**
* 非数かどうか判定します。
* @returns {boolean}
*/
isNaN() {
return isNaN(this.x) || isNaN(this.y) || isNaN(this.z) || isNaN(this.w);
}
/**
* 有限かどうか判定します。
* @returns {boolean}
*/
isFinite() {
return isFinite(this.x) && isFinite(this.y) && isFinite(this.z) && isFinite(this.w);
}
/**
* 実数かどうか判定します。
* @returns {boolean}
*/
isRealNumber() {
return !this.isNaN() && this.isFinite();
}
/**
* @typedef {Object} S3NormalVector
* @property {S3Vector} normal 平面の法線
* @property {S3Vector} tangent UV座標による接線
* @property {S3Vector} binormal UV座標による従法線
*/
/**
* 3点を通る平面の法線、接線、従法線を計算します。
* A, B, C の3点を通る平面の法線と、UV座標による接線、従法線を求めます。
* A, B, C の3点の時計回りが表だとした場合、表方向へ延びる法線となります。
* @param {S3Vector} posA 点A
* @param {S3Vector} posB 点B
* @param {S3Vector} posC 点C
* @param {S3Vector} [uvA] UV座標A
* @param {S3Vector} [uvB] UV座標B
* @param {S3Vector} [uvC] UV座標C
* @returns {S3NormalVector}
*/
static getNormalVector(posA, posB, posC, uvA, uvB, uvC) {
let N;
while (1) {
const P0 = posA.getDirection(posB);
const P1 = posA.getDirection(posC);
try {
N = P0.cross(P1).normalize();
} catch (e) {
// 頂点の位置が直行しているなどのエラー処理
N = new S3Vector(0.3333, 0.3333, 0.3333);
break;
}
if (uvA === undefined && uvB === undefined && uvC === undefined) {
// UV値がない場合はノーマルのみ返す
break;
}
// 接線と従法線を計算するにあたり、以下のサイトを参考にしました。
// http://sunandblackcat.com/tipFullView.php?l=eng&topicid=8
// https://stackoverflow.com/questions/5255806/how-to-calculate-tangent-and-binormal
// http://www.terathon.com/code/tangent.html
const st0 = uvA.getDirection(uvB);
const st1 = uvA.getDirection(uvC);
let q;
try {
// 接線と従法線を求める
q = 1.0 / st0.cross(st1).z;
const Tx = q * (st1.y * P0.x - st0.y * P1.x);
const Ty = q * (st1.y * P0.y - st0.y * P1.y);
const Tz = q * (st1.y * P0.z - st0.y * P1.z);
const Bx = q * (-st1.x * P0.x + st0.x * P1.x);
const By = q * (-st1.x * P0.y + st0.x * P1.y);
const Bz = q * (-st1.x * P0.z + st0.x * P1.z);
const T = new S3Vector(Tx, Ty, Tz); // Tangent 接線
const B = new S3Vector(Bx, By, Bz); // Binormal 従法線
return {
normal: N,
tangent: T.normalize(),
binormal: B.normalize()
};
/*
// 接線と従法線は直行していない
// 直行している方が行列として安定している。
// 以下、Gram-Schmidtアルゴリズムで直行したベクトルを作成する場合
const nT = T.sub(N.mul(N.dot(T))).normalize();
const w = N.cross(T).dot(B) < 0.0 ? -1.0 : 1.0;
const nB = N.cross(nT).mul(w);
return {
normal : N,
tangent : nT,
binormal : nB
}
*/
} catch (e) {
break;
}
}
return {
normal: N,
tangent: null,
binormal: null
};
}
/**
* 3点が時計回りか判定します。
* @param {S3Vector} A
* @param {S3Vector} B
* @param {S3Vector} C
* @returns {boolean|null} 時計回り:true、反時計回り:false、判定不可:null
*/
static isClockwise(A, B, C) {
const v1 = A.getDirection(B).setZ(0);
const v2 = A.getDirection(C).setZ(0);
const type = v1.cross(v2).z;
if (type === 0) {
return null;
} else if (type > 0) {
return true;
} else {
return false;
}
}
}
/**
* 0
* @type S3Vector
*/
S3Vector.ZERO = new S3Vector(0.0, 0.0, 0.0);