@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
1,230 lines (1,057 loc) • 27.7 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.
*
* Backed by a `Float64Array` of length 3, so instances are usable directly as typed-array views and indexed access `v[0]`, `v[1]`, `v[2]` aliases `v.x`, `v.y`, `v.z`.
*
* @implements Iterable<number>
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class Vector3 extends Float64Array {
/**
*
* @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');
super(3);
this[0] = x;
this[1] = y;
this[2] = z;
/**
* Dispatches ( x, y, z, old_x, old_y, old_z )
* @readonly
* @type {Signal<number,number,number,number,number,number>}
*/
this.onChanged = new Signal();
}
/**
* Ensure that built-in `TypedArray` methods (`map`, `slice`, `subarray`, ...) do not
* try to construct a `Vector3` via the buffer-form constructor.
* @returns {Float64ArrayConstructor}
*/
static get [Symbol.species]() {
return Float64Array;
}
/**
*
* @returns {number}
*/
get x() {
return this[0];
}
/**
*
* @param {number} v
*/
set x(v) {
this[0] = v;
}
/**
*
* @returns {number}
*/
get y() {
return this[1];
}
/**
*
* @param {number} v
*/
set y(v) {
this[1] = v;
}
/**
*
* @returns {number}
*/
get z() {
return this[2];
}
/**
*
* @param {number} v
*/
set z(v) {
this[2] = v;
}
/**
*
* @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[0];
array[offset + 1] = this[1];
array[offset + 2] = this[2];
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[0];
const oldY = this[1];
const oldZ = this[2];
if (x !== oldX || y !== oldY || z !== oldZ) {
// change has occurred
this[0] = x;
this[1] = y;
this[2] = 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[1], this[2]);
}
/**
*
* @param {number} v
* @returns {this}
*/
setY(v) {
return this.set(this[0], v, this[2]);
}
/**
*
* @param {number} v
* @returns {this}
*/
setZ(v) {
return this.set(this[0], this[1], v);
}
/**
*
* @param {number} x
* @param {number} y
* @returns {this}
*/
setXY(x, y) {
return this.set(x, y, this[2]);
}
/**
*
* @param {number} x
* @param {number} z
* @returns {this}
*/
setXZ(x, z) {
return this.set(x, this[1], z);
}
/**
*
* @param {number} y
* @param {number} z
* @returns {this}
*/
setYZ(y, z) {
return this.set(this[0], 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[0] + x,
this[1] + y,
this[2] + z
);
}
/**
*
* `this = this + other * scale`
*
* @param {Vector3} other
* @param {number} scale
* @returns {this}
*/
addScaled(other, scale) {
return this._add(
other.x * scale,
other.y * scale,
other.z * scale
);
}
/**
*
* @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[0] - x;
const _y = this[1] - y;
const _z = this[2] - 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[0] * x, this[1] * y, this[2] * 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[0] / x,
this[1] / y,
this[2] / 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[0] + val, this[1] + val, this[2] + 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[0], this[1], this[2]);
}
/**
*
* @param {number} val
* @returns {this}
*/
multiplyScalar(val) {
assert.isNumber(val, "val");
assert.notNaN(val, "val");
return this.set(this[0] * val, this[1] * val, this[2] * val);
}
/**
*
* @returns {boolean}
*/
isZero() {
return this[0] === 0
&& this[1] === 0
&& this[2] === 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[0]),
Math.abs(this[1]),
Math.abs(this[2])
);
}
/**
*
* @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[0], this[1], this[2]);
}
/**
* 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[0], this[1], this[2]);
}
/**
* Normalizes the vector, preserving its direction, but making magnitude equal to 1
* @returns {this}
*/
normalize() {
// NOTE: call v3_length directly rather than via `this.length()`.
// Float64Array exposes `length` as an exotic instance property that V8 inlines
// through a fast path which bypasses the prototype chain; once an optimized
// function caller has been specialized for Float64Array shape, `this.length`
// is read as the array length (a number) rather than our prototype method.
const l = v3_length(this[0], this[1], this[2]);
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[0],
-this[1],
-this[2]
);
}
/**
*
* @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[0], this[1], this[2],
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[0] - x,
this[1] - y,
this[2] - z
);
}
/**
* Angle between two vectors (co-planar) in radians
* @param {Vector3} other
* @returns {number}
*/
angleTo(other) {
return v3_angle_between(
this[0], this[1], this[2],
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[0];
const y = this[1];
const z = this[2];
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[0]),
sign(this[1]),
sign(this[2])
);
}
/**
* 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[0];
const y = this[1];
const z = this[2];
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[0];
const y = this[1];
const z = this[2];
// 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[0];
const y = this[1];
const z = this[2];
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[0] === x && this[1] === y && this[2] === 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[0], x, tolerance)
&& epsilonEquals(this[1], y, tolerance)
&& epsilonEquals(this[2], z, tolerance);
}
/**
* Apply component-wise `Math.round`
* @returns {this}
*/
round() {
return this.set(
Math.round(this[0]),
Math.round(this[1]),
Math.round(this[2]),
);
}
/**
* Apply component-wise `Math.floor`
* @returns {this}
*/
floor() {
const x = Math.floor(this[0]);
const y = Math.floor(this[1]);
const z = Math.floor(this[2]);
return this.set(x, y, z);
}
/**
* Apply component-wise `Math.ceil`
* @returns {this}
*/
ceil() {
return this.set(
Math.ceil(this[0]),
Math.ceil(this[1]),
Math.ceil(this[2]),
);
}
/**
* Project this vector onto another
* @param {Vector3} other
* @returns {this}
*/
projectOntoVector3(other) {
const x0 = this[0];
const y0 = this[1];
const z0 = this[2];
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[0], this[1], this[2]);
this.onChanged.add(processor, thisArg);
return this;
}
toJSON() {
return {
x: this[0],
y: this[1],
z: this[2]
};
}
/**
*
* @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[0]}, y:${this[1]}, z:${this[2]} }`;
}
/**
*
* @param {BinaryBuffer} buffer
* @deprecated
*/
toBinaryBuffer(buffer) {
buffer.writeFloat64(this[0]);
buffer.writeFloat64(this[1]);
buffer.writeFloat64(this[2]);
}
/**
*
* @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[0]);
buffer.writeFloat32(this[1]);
buffer.writeFloat32(this[2]);
}
/**
*
* @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[0]);
const y = computeHashFloat(this[1]);
const z = computeHashFloat(this[2]);
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 = new Vector3(0, 0, 0);
/**
* Useful for setting scale
* @readonly
* @type {Vector3}
*/
Vector3.one = new Vector3(1, 1, 1);
/**
* Useful for setting scale
* @readonly
* @type {Vector3}
*/
Vector3.minus_one = new Vector3(-1, -1, -1);
/**
* @readonly
* @type {Vector3}
*/
Vector3.up = new Vector3(0, 1, 0);
/**
* @readonly
* @type {Vector3}
*/
Vector3.down = new Vector3(0, -1, 0);
/**
* @readonly
* @type {Vector3}
*/
Vector3.left = new Vector3(-1, 0, 0);
/**
* @readonly
* @type {Vector3}
*/
Vector3.right = new Vector3(1, 0, 0);
/**
* @readonly
* @type {Vector3}
*/
Vector3.forward = new Vector3(0, 0, 1);
/**
* @readonly
* @type {Vector3}
*/
Vector3.back = new Vector3(0, 0, -1);
/**
* @readonly
* @type {boolean}
*/
Vector3.prototype.isVector3 = true;
/**
* @readonly
* @type {string}
*/
Vector3.typeName = "Vector3";
export default Vector3;