UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

1,230 lines (1,057 loc) • 27.7 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. * * 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;