UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

1,221 lines (1,046 loc) • 27 kB
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;