はじめに
ベクトルの自作を作ってみる実験のようななにか。自分でクラスを作ってみるのは楽しいです。好きなように仕様を設定できるので。与えられた環境に不自由さを感じたら、自分で作ってみましょう。楽しいです。
コード全文
名前は暫定的にVectaとしました。閉じているので移植は問題ないです。
class Vecta{
constructor(a=0,b=0,c=0){
this.x = a;
this.y = b;
this.z = c;
}
set(){
// 実はこれのimmutable版がcreateだが、
// setがcreate的な挙動をするのは気持ち悪いのでやらない。
const res = Vecta.validate(...arguments);
this.x = res.x; this.y = res.y; this.z = res.z;
return this;
}
copy(){
return new Vecta(this.x, this.y, this.z);
}
show(directConsole = false){
// trueの場合は直接コンソールに出す
const info = `${this.x}, ${this.y}, ${this.z}`;
if(directConsole){
console.log(info);
}
return info;
}
array(){
// 地味にあった方がいいかもしれない
return [this.x, this.y, this.z];
}
add(){
const res = Vecta.validate(...arguments);
if(res.im){
return new Vecta(this.x + res.x, this.y + res.y, this.z + res.z);
}
this.x += res.x; this.y += res.y; this.z += res.z;
return this;
}
sub(){
const res = Vecta.validate(...arguments);
if(res.im){
return new Vecta(this.x - res.x, this.y - res.y, this.z - res.z);
}
this.x -= res.x; this.y -= res.y; this.z -= res.z;
return this;
}
mult(){
const res = Vecta.validate(...arguments);
if(res.im){
return new Vecta(this.x * res.x, this.y * res.y, this.z * res.z);
}
this.x *= res.x; this.y *= res.y; this.z *= res.z;
return this;
}
div(){
const res = Vecta.validate(...arguments);
// ゼロ割は雑に回避
if(Math.abs(res.x) < Number.EPSILON){ res.x = Number.EPSILON; }
if(Math.abs(res.y) < Number.EPSILON){ res.y = Number.EPSILON; }
if(Math.abs(res.z) < Number.EPSILON){ res.z = Number.EPSILON; }
if(res.im){
return new Vecta(this.x / res.x, this.y / res.y, this.z / res.z);
}
this.x /= res.x; this.y /= res.y; this.z /= res.z;
return this;
}
cross(){
const res = Vecta.validate(...arguments);
if(res.im){
return this.copy().cross(res.x, res.y, res.z, false);
}
const {x,y,z} = this;
this.x = y * res.z - z * res.y;
this.y = z * res.x - x * res.z;
this.z = x * res.y - y * res.x;
return this;
}
angleBetween(v){
// 絶対値
// vはベクトル想定。余計な仕様を作りたくない。
const crossMag = this.cross(v, true).mag();
const dotValue = this.dot(v);
const theta = Math.atan2(crossMag, dotValue);
return theta;
}
angleTo(){
// axisから見た場合の符号付き角度。axisは成分指定可能(列挙のみ)。
// axisがゼロベクトルもしくは未定義の場合は(0,0,1)とする
// vはベクトル想定。ベクトルを2つも取るのでvはベクトルでないとさすがに無理。
const res = Vecta.validateForAngleTo(...arguments);
const sign = Math.sign(this.cross(res.v, true).dot(res.axis));
const theta = this.angleBetween(res.v);
return (sign < 0 ? sign : 1) * theta;
}
rotate(){
// axisの周りにangleだけ回転。axisは成分指定可能(列挙のみ)
// axisがゼロベクトルの場合は(0,0,1)とする
// 素直にロドリゲス掛けるだけです。GLSLとか意味不明なこと考えなくていいです。
const res = Vecta.validateForScalar(...arguments);
if(res.x*res.x + res.y*res.y + res.z*res.z < Number.EPSILON){
res.x = 0; res.y = 0; res.z = 0;
}
if(res.im){
return this.copy().rotate(res.x, res.y, res.z, res.s, false);
}
// res.imがfalseの場合は自分を変化させる
const axis = new Vecta(res.x, res.y, res.z);
axis.normalize();
const C = Math.cos(res.s);
const OC = 1-Math.cos(res.s);
const S = Math.sin(res.s);
const {x, y, z} = axis;
const mat = [
C + OC*x*x, OC*x*y - S*z, OC*x*z + S*y,
OC*x*y + S*z, C + OC*y*y, OC*y*z - S*x,
OC*x*z - S*y, OC*y*z + S*x, C + OC*z*z
];
const x1 = mat[0]*this.x + mat[1]*this.y + mat[2]*this.z;
const y1 = mat[3]*this.x + mat[4]*this.y + mat[5]*this.z;
const z1 = mat[6]*this.x + mat[7]*this.y + mat[8]*this.z;
this.set(x1, y1, z1);
return this;
}
addScalar(){
// 要するにvの定数倍を足すとかそういう処理
// かゆいところに手を伸ばすための関数
const res = Vecta.validateForScalar(...arguments);
if(res.im){
return new Vecta(
this.x + res.x * res.s, this.y + res.y * res.s, this.z + res.z * res.s
);
}
this.x += res.x * res.s;
this.y += res.y * res.s;
this.z += res.z * res.s;
return this;
}
lerp(){
// 対象と補間割合。割合が0なら自分、1なら相手。
const res = Vecta.validateForScalar(...arguments);
if(res.im){
return this.copy().lerp(res.x, res.y, res.z, res.s, false);
}
const {x,y,z} = this;
this.x = (1-res.s) * x + res.s * res.x;
this.y = (1-res.s) * y + res.s * res.y;
this.z = (1-res.s) * z + res.s * res.z;
return this;
}
dot(){
// 引数は割と自由で。1,2,3とかでもできるようにしましょ。
const res = Vecta.validate(...arguments);
return this.x * res.x + this.y * res.y + this.z * res.z;
}
dist(){
const res = Vecta.validate(...arguments);
return Math.hypot(this.x - res.x, this.y - res.y, this.z - res.z);
}
mag(){
return Math.sqrt(this.magSq());
}
magSq(){
return this.x * this.x + this.y * this.y + this.z * this.z;
}
normalize(){
const m = this.mag();
if(m < Number.EPSILON){
// ゼロの場合はゼロベクトルにする
return new Vecta(0,0,0);
}
return this.div(m);
}
static create(){
const res = Vecta.validate(...arguments);
return new Vecta(res.x, res.y, res.z);
}
static validate(){
// 長さ1,3の場合はfalseを追加
const args = [...arguments];
if(args.length === 1){
return Vecta.validate(args[0], false);
} else if(args.length === 3){
return Vecta.validate(args[0], args[1], args[2], false);
}else if(args.length === 2){
// 長さ2の場合はベクトルか数か配列。数の場合は全部一緒。
if(args[0] instanceof Vecta){
return {x:args[0].x, y:args[0].y, z:args[0].z, im: args[1]};
}else if(typeof(args[0]) === 'number'){
return {x:args[0], y:args[0], z:args[0], im:args[1]};
}else if(Array.isArray(args[0])){
return {x:args[0][0], y:args[0][1], z:args[0][2], im:args[1]};
}
}else if(args.length === 4){
// 長さ4の場合は数限定。
if(typeof(args[0]) === 'number'){
return {x:args[0], y:args[1], z:args[2], im:args[3]};
}
}
return {x:0, y:0, z:0, im:false}
}
static validateForAngleTo(){
const args = [...arguments];
if(args.length === 2){
// 長さ2の場合は2つ目がベクトルならゼロの場合にそれを回避する
if(args[1] instanceof Vecta){
if(args[1].magSq() < Number.EPSILON){
return {v:args[0], axis:new Vecta(0,0,1)};
}
return {v:args[0], axis:args[1]};
}else if(Array.isArray(args[1])){
return Vecta.validateForAngleTo(args[0], new Vecta(args[1][0], args[1][1], args[1][2]));
}
}else if(args.length === 4){
// 長さ4の場合は後半の3つの数でベクトルを作る
if(typeof(args[1]) === 'number'){
return Vecta.validateForAngleTo(args[0], new Vecta(args[1], args[2], args[3]));
}
}
return {v:args[0], axis:new Vecta(0,0,1)};
}
static validateForScalar(){
// 想定してるのはaxis,angleもしくはvector,scalar
const args = [...arguments];
// 長さ2,4の場合はfalseを追加
if(args.length === 2){
return Vecta.validateForScalar(args[0], args[1], false);
}else if(args.length === 4){
return Vecta.validateForScalar(args[0], args[1], args[2], args[3], false);
}else if(args.length === 3){
// 長さ3の場合は...ベクトルか配列。
if(typeof(args[1]) === 'number'){
if(args[0] instanceof Vecta){
return {x:args[0].x, y:args[0].y, z:args[0].z, s:args[1], im:args[2]};
}else if(Array.isArray(args[0])){
return {x:args[0][0], y:args[0][1], z:args[0][2], s:args[1], im:args[2]};
}
}
}else if(args.length === 5){
// 長さ5の場合は数でベクトルを作る。
if(typeof(args[0]) === 'number' && typeof(args[3]) === 'number'){
return {x:args[0], y:args[1], z:args[2], s:args[3], im:args[4]};
}
}
return {x:0, y:0, z:0, s:0, im:false};
}
static getOrtho(v){
// 雑に直交する単位ベクトルを取る。slerpはこれがあると楽。
if(v.magSq() < Number.EPSILON){
return Vecta.create(0,0,1);
}
if(v.x > 0){
return Vecta.create(v.y, -v.x, 0).normalize();
}
return Vecta.create(0, v.z, -v.y).normalize();
}
static random2D(){
// ランダムで円周上の単位ベクトルを取る
const t = Math.random()*Math.PI*2;
return Vecta.create(Math.cos(t), Math.sin(t));
}
static random3D(){
// ランダムで球面上の単位ベクトルを取る
const s = Math.acos(1-Math.random()*2);
const t = Math.random()*Math.PI*2;
return Vecta.create(Math.sin(s)*Math.cos(t), Math.sin(s)*Math.sin(t), Math.cos(s));
}
static random3Dvariation(axis, angle, directionFunc){
// 関数部分以外は一緒なので統一する
if(axis.magSq()<Number.EPSILON){
axis = new Vecta(0,0,1);
}
const zVec = axis.copy().normalize();
const xVec = Vecta.getOrtho(zVec);
const yVec = zVec.cross(xVec, true);
const properAngle = Math.max(Math.min(angle, Math.PI), 0);
const s = directionFunc(Math.random(), properAngle);
const t = Math.random()*Math.PI*2;
return new Vecta().addScalar(zVec, Math.cos(s)).addScalar(xVec, Math.sin(s)*Math.cos(t)).addScalar(yVec, Math.sin(s)*Math.sin(t));
}
static random3Dinside(axis, angle){
// axis方向、angleより内側の球面上からランダムに取得する。
const directionFunc = (rdm, properAngle) => {
return Math.acos(1-rdm*(1-Math.cos(properAngle)));
}
return Vecta.random3Dvariation(axis, angle, directionFunc);
}
static random3Doutside(axis, angle){
// axis方向、angleより外側の球面上からランダムに取得する。
const directionFunc = (rdm, properAngle) => {
return Math.acos(Math.cos(properAngle) - (1+Math.cos(properAngle))*rdm);
}
return Vecta.random3Dvariation(axis, angle, directionFunc);
}
}
p5のベクトルクラスの問題点
p5のベクトルクラスの不自由な点を解消しようと思って作りました。自作ライブラリの方ではこれとは別にベクトルクラス(Vec3とかいう名前)を作って使っているのですが、p5に影響されて作ったせいで使いづらい点が目立つのでそこら辺の改善を意識しました。
immutableかどうかが曖昧なところ
たとえばaddやsubなど、ほとんどの関数はnon-immutable,つまり対象となるベクトルを変化させる内容ですが、ただひとつ、crossだけimmutableになっています。つまり、作用させたベクトルとは別のベクトルを生成する仕様になっています。これは使ってみると分かりますが、crossは圧倒的にこの手の使い方が多いからです(2本の軸を取ってからそれらに直交する方向を取るなどの使い方が圧倒的に多い)。しかし自分としては整合性が取れてなくてちょっと具合が悪いように感じるので、
- immutableかどうかが問題になるすべての関数をnon-immutableのデフォルトで統一する
という規約を設けました。ゆえにcrossはtrueをわざわざ指定しないとimmutableにならないわけです。ただどっちなのかわからずモヤモヤするよりはマシです。
immutableで使おうとするとめんどくさいことになる
p5のたとえばaddをimmutableで使おうとする場合、
v = v1.copy().add(v2);
のようにコピーを取ってaddを適用するか、
v = p5.Vector.add(v1, v2);
のようにstatic関数を使わないといけないです。これはめんどくさいです。そこでimmutableをフラグで設けて、
v = v1.add(v2, true);
のようにすることでv1を変えずに新しいベクトルを取れるようにしました。代わりにstaticは引数のバリデーション関数を用意することにしました。ベクトルの関数は引数のパターンが似通っているものが多いので、割と使いまわしがききます。jsにはオーバーロードがない代わりに、argumentsを使って引数配列を取得できるので、この手のコードは書きやすいです。いいマナーかどうかはおいといて、とりあえずこれでいくことにしました。
また、static形式でいくつも同じ名前の関数が存在していると単純に邪魔というのもあります。
ちなみにstaticのcreateは実質的にはsetのimmutable版ですが、setがcreate的な挙動をするのは不自然なので、別の関数としました。
rotateが2D止まり
p5のベクトルのrotateは2Dにしか対応していません。軸周りの回転が無いのは不便なので用意しました。
angleBetweenが「between」という名称なのに負の数を返す
betweenは「間」という意味なのに負の数を返すangleBetweenが不自然なように感じたので、これは絶対値を返すものとし、それとは別に軸に対してそれを正の方向から見た場合の角度の変化という形で符号付きの値を返す、angleToという関数を用意しました。デフォルトの場合の軸は(0,0,1)なので2次元でも使えます。なおこれらの関数に関しては第一引数はベクトルで限定しています。
工夫した点
以下は工夫したポイントについてです。
球面上のランダムベクトル関数をちょっとだけ拡張
軸と角度を設けて、その軸の周りの一定角度以上、ないしは角度以下、の領域からランダムで点を取得する、random3Dの変種のような関数を用意しました。こういうのを勝手に追加できるのは楽しいですね。具体的にはこんな風になります。
このように上半分をカットしたような領域からランダムで選ばれます。この辺は、
p5.jsで円環状の領域の上の均等分布を実装する
ここで紹介している技法を使えばp5の範疇でも簡単に作れるので試してみてください。ここに上げてあるのはあくまでひとつの実装例です。
show関数で楽に内容を取得
成分の列挙は割とめんどくさいです。p5のベクトルにはtoStringがあるんですがいちいちconsoleにぶち込むのは手間です。そこで成分をstringsで返す関数を用意したうえで、引数によってはそれを関数内でコンソール出力してくれるshowという関数を用意しました。これなら
v = new Vecta(1,2,3);
v.show(true); // 1, 2, 3
とするだけで内容をチェックできます。非常に便利です。
addScalar
ベクトルは単に加えるだけでなく、それの定数倍を加えたい場合というのが割と、あります。その場合にそのベクトルを変えずに実行したい場合、p5の仕様だと、
v.add(v1.copy().mult(m));
とか
v.add(v1.x*m, v1.y*m, v1.z*m);
とかしないといけなくて割と面倒です。そもそもこの程度のことで新しいベクトルを作りたくないです。そこで、
v.addScalar(v1, m);
という形でサクッと実行できるようにしました。
ゼロ割について
divについてはゼロ割かチェックを入れる、エラー処理、考えたんですが、安易にそういうコードを書くと無限エラー地獄になったり容易に止まっちゃったりして面倒なので、0に近い場合はNumber.EPSILONで置き換えることとしました。この場合0/0の結果は0となるわけです。この辺はきちんとしたライブラリでは然るべく処理しています。なおjsの場合1/0はInfinity, 0/0の結果はNaNとなります。NaNは厄介なので出来るだけ回避しています(平方根のところとかは分かりませんが)。
後は特にないですね...ちなみにベクトルのslerpを導入するのも考えたんですが、当面は不要なので入れませんでした。
使用例
function test_mult(){
console.log("------mult------");
const v0 = new Vecta(1,2,3);
v0.mult(4).show(true); // 4,8,12
v0.mult(2,3,4).show(true); // 8,24,48
v0.mult([1,3,5]).show(true); // 8.72,240
v0.mult(new Vecta(9,4,2)).show(true); // 72,288,480
const v1 = new Vecta(3,2,1);
v1.mult(4,true).show(true); // 12,8,4
v1.mult(4,9,13,true).show(true); // 12,18,13
v1.mult([8,4,2],true).show(true); // 24,8,2
v1.mult(new Vecta(10,20,30),true).show(true); // 30,40,30
v1.show(true); // 3,2,1
}
おわりに
自作クラスを作るのは楽しいです。この程度ならそれほど時間もかからずに作れるので、既存の枠組みで出来ないことがあって不自由なように感じたら、作ることに挑戦してみるといいかもしれないです。自分で作ったクラスには愛着がわきます。とても楽しい!
ここまでお読みいただいてありがとうございました。