@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
1,221 lines (1,046 loc) • 27 kB
JavaScript
import { assert } from "../assert.js";
import Signal from "../events/signal/Signal.js";
import { EPSILON } from "../math/EPSILON.js";
import { epsilonEquals } from "../math/epsilonEquals.js";
import { sign } from "../math/sign.js";
import { computeHashFloat } from "../primitives/numbers/computeHashFloat.js";
import { v3_angle_between } from "./vec3/v3_angle_between.js";
import { v3_distance } from "./vec3/v3_distance.js";
import { v3_dot } from "./vec3/v3_dot.js";
import { v3_length } from "./vec3/v3_length.js";
import { v3_length_sqr } from "./vec3/v3_length_sqr.js";
import { v3_lerp } from "./vec3/v3_lerp.js";
import { v3_slerp } from "./vec3/v3_slerp.js";
/**
* Class representing a 3D vector. A 3D vector is an ordered triplet of numbers (labeled x, y, and z), which can be used to represent a number of things, such as:
*
* A point in 3D space.
* A direction and length in 3D space. The length will always be the Euclidean distance (straight-line distance) from `(0, 0, 0)` to `(x, y, z)` and the direction is also measured from `(0, 0, 0)` towards `(x, y, z)`.
* Any arbitrary ordered triplet of numbers.
* There are other things a 3D vector can be used to represent, such as momentum vectors and so on, however these are the most common uses.
*
* Iterating through a Vector3 instance will yield its components `(x, y, z)` in the corresponding order.
*
* @implements Iterable<number>
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class Vector3 {
/**
*
* @param {number} [x=0]
* @param {number} [y=0]
* @param {number} [z=0]
*/
constructor(x = 0, y = 0, z = 0) {
assert.isNumber(x, 'x');
assert.notNaN(x, 'x');
assert.isNumber(y, 'y');
assert.notNaN(y, 'y');
assert.isNumber(z, 'z');
assert.notNaN(z, 'z');
/**
* Do not assign directly, use {@link set} method instead
* @readonly
* @type {number}
*/
this.x = x;
/**
* Do not assign directly, use {@link set} method instead
* @readonly
* @type {number}
*/
this.y = y;
/**
* Do not assign directly, use {@link set} method instead
* @readonly
* @type {number}
*/
this.z = z;
/**
* Dispatches ( x, y, z, old_x, old_y, old_z )
* @readonly
* @type {Signal<number,number,number,number,number,number>}
*/
this.onChanged = new Signal();
}
/**
*
* @return {number}
*/
get 0() {
return this.x;
}
/**
*
* @return {number}
*/
get 1() {
return this.y;
}
/**
*
* @return {number}
*/
get 2() {
return this.z;
}
/**
*
* @param {number} v
*/
set 0(v) {
this.x = v;
}
/**
*
* @param {number} v
*/
set 1(v) {
this.y = v;
}
/**
*
* @param {number} v
*/
set 2(v) {
this.z = v;
}
/**
*
* @return {Generator<number>}
*/
* [Symbol.iterator]() {
yield this.x;
yield this.y;
yield this.z;
}
/**
*
* @param {number[]|Float32Array} array
* @param {number} [offset]
* @returns {this}
*/
fromArray(array, offset = 0) {
assert.defined(array, "array");
assert.isNonNegativeInteger(offset, "offset");
return this.set(
array[offset],
array[offset + 1],
array[offset + 2]
);
}
/**
* @param {number[]|Float32Array|ArrayLike} [array]
* @param {number} [offset]
* @returns {number[]}
*/
toArray(array = [], offset = 0) {
assert.isNonNegativeInteger(offset, "offset");
array[offset] = this.x;
array[offset + 1] = this.y;
array[offset + 2] = this.z;
return array;
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @returns {this}
*/
set(x, y, z) {
assert.isNumber(x, "x");
assert.isNumber(y, "y");
assert.isNumber(z, "z");
assert.notNaN(x, "x");
assert.notNaN(y, "y");
assert.notNaN(z, "z");
const oldX = this.x;
const oldY = this.y;
const oldZ = this.z;
if (x !== oldX || y !== oldY || z !== oldZ) {
// change has occurred
this.x = x;
this.y = y;
this.z = z;
if (this.onChanged.hasHandlers()) {
this.onChanged.send6(
x, y, z,
oldX, oldY, oldZ
);
}
}
return this;
}
/**
*
* @param {number} v
* @returns {this}
*/
setScalar(v) {
return this.set(v, v, v);
}
/**
*
* @param {number} v
* @returns {this}
*/
setX(v) {
return this.set(v, this.y, this.z);
}
/**
*
* @param {number} v
* @returns {this}
*/
setY(v) {
return this.set(this.x, v, this.z);
}
/**
*
* @param {number} v
* @returns {this}
*/
setZ(v) {
return this.set(this.x, this.y, v);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
setXY(x, y) {
return this.set(x, y, this.z);
}
/**
*
* @param {number} x
* @param {number} z
* @returns {this}
*/
setXZ(x, z) {
return this.set(x, this.y, z);
}
/**
*
* @param {number} y
* @param {number} z
* @returns {this}
*/
setYZ(y, z) {
return this.set(this.x, y, z);
}
/**
*
* @param {Vector3} a
* @param {Vector3} b
* @returns {this}
*/
addVectors(a, b) {
const x = a.x + b.x;
const y = a.y + b.y;
const z = a.z + b.z;
return this.set(x, y, z);
}
/**
*
* @param {Vector3} other
* @returns {this}
*/
add(other) {
return this._add(other.x, other.y, other.z);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @returns {this}
*/
_add(x, y, z) {
return this.set(this.x + x, this.y + y, this.z + z);
}
/**
*
* @param {Vector3} a
* @param {Vector3} b
* @returns {this}
*/
subVectors(a, b) {
const x = a.x - b.x;
const y = a.y - b.y;
const z = a.z - b.z;
return this.set(x, y, z);
}
/**
*
* @param {Vector3} other
* @returns {this}
*/
sub(other) {
return this._sub(other.x, other.y, other.z);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @returns {this}
*/
_sub(x, y, z) {
const _x = this.x - x;
const _y = this.y - y;
const _z = this.z - z;
return this.set(_x, _y, _z);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @returns {this}
*/
_multiply(x, y, z) {
return this.set(this.x * x, this.y * y, this.z * z);
}
/**
*
* @param {Vector3} other
* @returns {this}
*/
multiply(other) {
return this._multiply(other.x, other.y, other.z);
}
/**
*
* @param {Vector3} a
* @param {Vector3} b
* @returns {this}
*/
multiplyVectors(a, b) {
return this.set(
a.x * b.x,
a.y * b.y,
a.z * b.z
);
}
/**
* Component-wise division.
* @param {number} x
* @param {number} y
* @param {number} z
* @returns {this}
*/
_divide(x, y, z) {
return this.set(
this.x / x,
this.y / y,
this.z / z,
)
}
/**
* Component-wise division.
* @param {Vector3} other
* @returns {this}
*/
divide(other) {
return this._divide(other.x, other.y, other.z);
}
/**
* Component-wise division.
* `this = a / b`
* @param {Vector3} a
* @param {Vector3} b
* @return {Vector3}
*/
divideVectors(a, b) {
return this.set(
a.x / b.x,
a.y / b.y,
a.z / b.z
);
}
/**
* Add a scalar value to each component of the vector
* @param {number} val
* @returns {this}
*/
addScalar(val) {
return this.set(this.x + val, this.y + val, this.z + val);
}
/**
* Subtract scalar value from each component of the vector
* @param {number} val
* @returns {this}
*/
subScalar(val) {
return this.addScalar(-val);
}
/**
*
* @returns {Vector3}
*/
clone() {
return new Vector3(this.x, this.y, this.z);
}
/**
*
* @param {number} val
* @returns {this}
*/
multiplyScalar(val) {
assert.isNumber(val, "val");
assert.notNaN(val, "val");
return this.set(this.x * val, this.y * val, this.z * val);
}
/**
*
* @returns {boolean}
*/
isZero() {
return this.x === 0
&& this.y === 0
&& this.z === 0
;
}
/**
* Compute cross-product of two vectors. Result is written to this vector.
* @param {Vector3} other
* @returns {this}
*/
cross(other) {
return this.crossVectors(this, other);
}
/**
*
* @param {Vector3} first
* @param {Vector3} second
* @returns {this}
*/
crossVectors(first, second) {
const ax = first.x, ay = first.y, az = first.z;
const bx = second.x, by = second.y, bz = second.z;
return this._crossVectors(
ax, ay, az,
bx, by, bz
);
}
/**
*
* @param {number} ax
* @param {number} ay
* @param {number} az
* @param {number} bx
* @param {number} by
* @param {number} bz
* @returns {this}
*/
_crossVectors(ax, ay, az, bx, by, bz) {
const x = ay * bz - az * by;
const y = az * bx - ax * bz;
const z = ax * by - ay * bx;
return this.set(x, y, z);
}
/**
* Sets all components of the vector to absolute value (positive)
* @returns {this}
*/
abs() {
return this.set(
Math.abs(this.x),
Math.abs(this.y),
Math.abs(this.z)
);
}
/**
*
* @param {Vector3} v
* @returns {number}
*/
dot(v) {
return Vector3.dot(this, v);
}
/**
* Computes length(magnitude) of the vector
* @returns {number}
*/
length() {
return v3_length(this.x, this.y, this.z);
}
/**
* Computes squared length(magnitude) of the vector.
* Note: this operation is faster than computing length, because it doesn't involve computing square root
* @returns {number}
*/
lengthSqr() {
return v3_length_sqr(this.x, this.y, this.z);
}
/**
* Normalizes the vector, preserving its direction, but making magnitude equal to 1
* @returns {this}
*/
normalize() {
const l = this.length();
if (l === 0) {
//special case, can't normalize 0 length vector
return this;
}
const m = 1 / l;
return this.multiplyScalar(m);
}
/**
* @param {number} [squared_error]
* @return {boolean}
*/
isNormalized(squared_error = 1e-5) {
const length_sqr = this.lengthSqr();
return epsilonEquals(length_sqr, 1, squared_error);
}
/**
*
* @param {Vector3|{x:number,y:number,z:number}} other
* @returns {this}
*/
copy(other) {
return this.set(other.x, other.y, other.z);
}
/**
* Negates every component of the vector making it {-x, -y, -z}
* @returns {this}
*/
negate() {
return this.set(
-this.x,
-this.y,
-this.z
);
}
/**
*
* @param {Vector3} other
* @returns {number}
*/
distanceTo(other) {
return this._distanceTo(other.x, other.y, other.z);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @return {number}
*/
_distanceTo(x, y, z) {
return v3_distance(
this.x, this.y, this.z,
x, y, z
);
}
/**
* Squared distance between this vector and another. It is faster than computing real distance because no SQRT operation is needed.
* @param {Vector3} other
* @returns {number}
*/
distanceSqrTo(other) {
assert.defined(other, "other");
return this._distanceSqrTo(other.x, other.y, other.z);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @return {number}
*/
_distanceSqrTo(x, y, z) {
return v3_length_sqr(
this.x - x,
this.y - y,
this.z - z
);
}
/**
* Angle between two vectors (co-planar) in radians
* @param {Vector3} other
* @returns {number}
*/
angleTo(other) {
return v3_angle_between(
this.x, this.y, this.z,
other.x, other.y, other.z
);
}
/**
*
* @param {Quaternion} q
* @returns {this}
*/
applyQuaternion(q) {
// NOTE: the logic is inlines for speed
//transform point into quaternion
const x = this.x;
const y = this.y;
const z = this.z;
const qx = q.x;
const qy = q.y;
const qz = q.z;
const qw = q.w;
// calculate quat * vector
const ix = qw * x + qy * z - qz * y;
const iy = qw * y + qz * x - qx * z;
const iz = qw * z + qx * y - qy * x;
const iw = -qx * x - qy * y - qz * z;
// calculate result * inverse quat
const _x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
const _y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
const _z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
return this.set(_x, _y, _z);
}
/**
* Set components X,Y,Z to values 1,0 or -1 based on the sign of their original value.
* @example new Vector(5,0,-10).sign().equals(new Vector(1,0,-1)); //true
* @returns {this}
*/
sign() {
return this.set(
sign(this.x),
sign(this.y),
sign(this.z)
);
}
/**
* Linear interpolation
* @param {Vector3} other
* @param {number} fraction between 0 and 1
* @returns {this}
*/
lerp(other, fraction) {
return this.lerpVectors(this, other, fraction);
}
/**
*
* @param {Vector3} a
* @param {Vector3} b
* @param {number} fraction
* @returns {this}
*/
lerpVectors(a, b, fraction) {
v3_lerp(this, a.x, a.y, a.z, b.x, b.y, b.z, fraction);
return this;
}
/**
*
* @param {Vector3} other
* @param {number} fraction
* @return {this}
*/
slerp(other, fraction) {
return this.slerpVectors(this, other, fraction);
}
/**
* Spherical linear interpolation
* @param {Vector3} a
* @param {Vector3} b
* @param {number} fraction
* @returns {this}
*/
slerpVectors(a, b, fraction) {
v3_slerp(this, a.x, a.y, a.z, b.x, b.y, b.z, fraction);
return this;
}
/**
*
* @param {ArrayLike<number>|number[]|Float32Array} m4
* @returns {this}
*/
applyMatrix4(m4) {
const x = this.x;
const y = this.y;
const z = this.z;
const w = 1 / (m4[3] * x + m4[7] * y + m4[11] * z + m4[15]);
const _x = (m4[0] * x + m4[4] * y + m4[8] * z + m4[12]) * w;
const _y = (m4[1] * x + m4[5] * y + m4[9] * z + m4[13]) * w;
const _z = (m4[2] * x + m4[6] * y + m4[10] * z + m4[14]) * w;
return this.set(_x, _y, _z);
}
/**
* Assume current vector holds a direction, transform using a matrix to produce a new directional unit vector.
* Note that the vector is normalized
* @param {ArrayLike<number>|number[]|Float32Array} m4
* @returns {this}
*/
applyDirectionMatrix4(m4) {
const x = this.x;
const y = this.y;
const z = this.z;
// This is just 3x3 matrix multiplication
const _x = m4[0] * x + m4[4] * y + m4[8] * z;
const _y = m4[1] * x + m4[5] * y + m4[9] * z;
const _z = m4[2] * x + m4[6] * y + m4[10] * z;
// normalize the result
const _l = 1 / v3_length(_x, _y, _z);
return this.set(
_x * _l,
_y * _l,
_z * _l
);
}
/**
*
* @param {number[]|Float32Array} mat
* @returns {this}
*/
applyMatrix3(mat) {
const x = this.x;
const y = this.y;
const z = this.z;
const _x = mat[0] * x + mat[3] * y + mat[6] * z;
const _y = mat[1] * x + mat[4] * y + mat[7] * z;
const _z = mat[2] * x + mat[5] * y + mat[8] * z;
return this.set(_x, _y, _z);
}
/**
*
* @param {ArrayLike<number>|number[]|Float32Array} matrix4 4x4 transform matrix
* @returns {this}
*/
setFromMatrixPosition(matrix4) {
const _x = matrix4[12];
const _y = matrix4[13];
const _z = matrix4[14];
return this.set(_x, _y, _z);
}
/**
*
* @param {Vector3} other
* @returns {boolean}
*/
equals(other) {
return this._equals(other.x, other.y, other.z);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @return {boolean}
*/
_equals(x, y, z) {
return this.x === x && this.y === y && this.z === z;
}
/**
*
* @param {Vector3} other
* @param {number} [tolerance]
* @return {boolean}
*/
roughlyEquals(other, tolerance) {
return this._roughlyEquals(other.x, other.y, other.z, tolerance);
}
/**
*
* @param {number} x
* @param {number} y
* @param {number} z
* @param {number} [tolerance] acceptable deviation
* @return {boolean}
*/
_roughlyEquals(x, y, z, tolerance = EPSILON) {
return epsilonEquals(this.x, x, tolerance)
&& epsilonEquals(this.y, y, tolerance)
&& epsilonEquals(this.z, z, tolerance);
}
/**
* Apply component-wise `Math.round`
* @returns {this}
*/
round() {
return this.set(
Math.round(this.x),
Math.round(this.y),
Math.round(this.z),
);
}
/**
* Apply component-wise `Math.floor`
* @returns {this}
*/
floor() {
const x = Math.floor(this.x);
const y = Math.floor(this.y);
const z = Math.floor(this.z);
return this.set(x, y, z);
}
/**
* Apply component-wise `Math.ceil`
* @returns {this}
*/
ceil() {
return this.set(
Math.ceil(this.x),
Math.ceil(this.y),
Math.ceil(this.z),
);
}
/**
* Project this vector onto another
* @param {Vector3} other
* @returns {this}
*/
projectOntoVector3(other) {
const x0 = this.x;
const y0 = this.y;
const z0 = this.z;
const x1 = other.x;
const y1 = other.y;
const z1 = other.z;
return this._projectVectors(x0, y0, z0, x1, y1, z1);
}
/**
* Project first vector onto second one
* @param {number} x0
* @param {number} y0
* @param {number} z0
* @param {number} x1
* @param {number} y1
* @param {number} z1
* @returns {this}
*/
_projectVectors(
x0, y0, z0,
x1, y1, z1
) {
const d = v3_dot(x0, y0, z0, x1, y1, z1);
const length_sqr = (x1 * x1 + y1 * y1 + z1 * z1);
const m = d / length_sqr;
const x = x1 * m;
const y = y1 * m;
const z = z1 * m;
return this.set(x, y, z);
}
/**
* Convert spherical coordinates to cartesian
*
* We assume Y-up coordinate system.
*
* @param {number} radius
* @param {number} phi Also known as Azimuth
* @param {number} theta Also known as Elevation
* @returns {this}
*/
setFromSphericalCoords(radius, phi, theta) {
assert.isNumber(radius, 'radius');
assert.notNaN(radius, 'radius');
assert.isNumber(phi, 'phi');
assert.notNaN(phi, 'phi');
assert.isNumber(theta, 'theta');
assert.notNaN(theta, 'theta');
const sin_phi = Math.sin(phi);
const cos_phi = Math.cos(phi);
const sin_theta = Math.sin(theta);
const cos_theta = Math.cos(theta);
const _x = radius * sin_phi * sin_theta;
const _y = radius * cos_phi;
const _z = radius * sin_phi * cos_theta;
return this.set(_x, _y, _z);
}
/**
*
* @param {function} processor
* @param {*} [thisArg]
* @returns {Vector3}
*/
process(processor, thisArg) {
processor.call(thisArg, this.x, this.y, this.z);
this.onChanged.add(processor, thisArg);
return this;
}
toJSON() {
return {
x: this.x,
y: this.y,
z: this.z
};
}
/**
*
* @param {{x:number, y:number, z:number}|number} json
*/
fromJSON(json) {
if (typeof json === 'number') {
// special case where input is a number
this.setScalar(json);
} else {
this.copy(json);
}
}
toString() {
return `Vector3{ x:${this.x}, y:${this.y}, z:${this.z} }`;
}
/**
*
* @param {BinaryBuffer} buffer
* @deprecated
*/
toBinaryBuffer(buffer) {
buffer.writeFloat64(this.x);
buffer.writeFloat64(this.y);
buffer.writeFloat64(this.z);
}
/**
*
* @param {BinaryBuffer} buffer
* @deprecated
*/
fromBinaryBuffer(buffer) {
const x = buffer.readFloat64();
const y = buffer.readFloat64();
const z = buffer.readFloat64();
this.set(x, y, z);
}
/**
*
* @param {BinaryBuffer} buffer
* @deprecated
*/
toBinaryBufferFloat32(buffer) {
buffer.writeFloat32(this.x);
buffer.writeFloat32(this.y);
buffer.writeFloat32(this.z);
}
/**
*
* @param {BinaryBuffer} buffer
* @deprecated
*/
fromBinaryBufferFloat32(buffer) {
const x = buffer.readFloat32();
const y = buffer.readFloat32();
const z = buffer.readFloat32();
this.set(x, y, z);
}
hash() {
const x = computeHashFloat(this.x);
const y = computeHashFloat(this.y);
const z = computeHashFloat(this.z);
return x ^ (y << 1) ^ (z << 2);
}
/**
* Dot product
* @param {Vector3|Vector4} a
* @param {Vector3|Vector4} b
* @returns {number}
*/
static dot(a, b) {
return v3_dot(a.x, a.y, a.z, b.x, b.y, b.z);
}
/**
*
* @param {Vector3} a
* @param {Vector3} b
* @returns {number}
*/
static distance(a, b) {
return v3_length(a.x - b.x, a.y - b.y, a.z - b.z);
}
/**
*
* @param {number[]} input
* @param {number} [offset]
* @returns {Vector3}
*/
static fromArray(input, offset = 0) {
return new Vector3(
input[offset],
input[offset + 1],
input[offset + 2]
);
}
/**
*
* @param {number} value
* @returns {Vector3}
*/
static fromScalar(value) {
return new Vector3(value, value, value);
}
}
// Aliases
Vector3.prototype.distanceToSquared = Vector3.prototype.distanceSqrTo;
Vector3.prototype.lengthSq = Vector3.prototype.lengthSqr;
/**
* @readonly
* @deprecated Use {@link fromArray} instead
*/
Vector3.prototype.readFromArray = Vector3.prototype.fromArray;
/**
* @readonly
* @deprecated Use {@link toArray} instead
*/
Vector3.prototype.writeToArray = Vector3.prototype.toArray;
/**
* @readonly
* @deprecated use {@link toArray} instead
*/
Vector3.prototype.asArray = Vector3.prototype.toArray;
/**
* @readonly
* @type {Vector3}
*/
Vector3.zero = Object.freeze(new Vector3(0, 0, 0));
/**
* Useful for setting scale
* @readonly
* @type {Vector3}
*/
Vector3.one = Object.freeze(new Vector3(1, 1, 1));
/**
* Useful for setting scale
* @readonly
* @type {Vector3}
*/
Vector3.minus_one = Object.freeze(new Vector3(-1, -1, -1));
/**
* @readonly
* @type {Vector3}
*/
Vector3.up = Object.freeze(new Vector3(0, 1, 0));
/**
* @readonly
* @type {Vector3}
*/
Vector3.down = Object.freeze(new Vector3(0, -1, 0));
/**
* @readonly
* @type {Vector3}
*/
Vector3.left = Object.freeze(new Vector3(-1, 0, 0));
/**
* @readonly
* @type {Vector3}
*/
Vector3.right = Object.freeze(new Vector3(1, 0, 0));
/**
* @readonly
* @type {Vector3}
*/
Vector3.forward = Object.freeze(new Vector3(0, 0, 1));
/**
* @readonly
* @type {Vector3}
*/
Vector3.back = Object.freeze(new Vector3(0, 0, -1));
/**
* @readonly
* @type {boolean}
*/
Vector3.prototype.isVector3 = true;
/**
* @readonly
* @type {string}
*/
Vector3.typeName = "Vector3";
export default Vector3;