/** @type {number} */
const HALF_FIELD_OF_VIEW_DIVISOR = 2.0;
/** @type {number} */
const PROJECTION_SCALE_NUMERATOR = 1.0;
/** @type {number} */
const DEPTH_RANGE_NUMERATOR = 1.0;
/** @type {number} */
const PERSPECTIVE_Z_RANGE_MULTIPLIER = 2.0;
/** @type {number} */
const PERSPECTIVE_W_COMPONENT_SCALE = -1.0;
/** @type {number} */
const MATRIX_4x4_ELEMENT_COUNT = 16;
/** @type {number} */
const MATRIX_COLUMN_COUNT = 4;
/** @type {number} */
const MATRIX_ROW_COUNT = 4;
/** @type {number} */
const MATRIX_STRIDE = 4;
/** @type {number} */
const MIN_INVERTIBLE_DETERMINANT_ABS = 1e-12;
/** @type {number} */
const INVERSE_DETERMINANT_NUMERATOR = 1.0;
/**
* Utility class for working with 4x4 matrices in column-major order.
*/
export class Matrix4 {
/**
* Creates a new 4x4 identity matrix.
*
* @returns {Float32Array} - A new identity matrix.
*/
static createIdentity() {
const out = Matrix4.#createEmpty();
out[0] = 1;
out[5] = 1;
out[10] = 1;
out[15] = 1;
return out;
}
/**
* Creates a scale matrix.
*
* @param {number} scaleX - Scale along X.
* @param {number} scaleY - Scale along Y.
* @param {number} scaleZ - Scale along Z.
* @returns {Float32Array} - A new scale matrix.
*/
static createScale(scaleX, scaleY, scaleZ) {
if (typeof scaleX !== 'number'
|| typeof scaleY !== 'number'
|| typeof scaleZ !== 'number') {
throw new TypeError('Matrix4.createScale expects numeric arguments.');
}
const out = Matrix4.#createEmpty();
out[0] = scaleX;
out[5] = scaleY;
out[10] = scaleZ;
out[15] = 1;
return out;
}
/**
* Creates a perspective projection matrix.
*
* @param {number} fieldOfViewRadians - Vertical field of view in radians.
* @param {number} aspectRatio - Viewport aspect ratio (width / height).
* @param {number} near - Near clipping plane, must be > 0.
* @param {number} far - Far clipping plane, must be > near.
* @returns {Float32Array} - A new perspective projection matrix.
*/
static createPerspective(fieldOfViewRadians, aspectRatio, near, far) {
if (typeof fieldOfViewRadians !== 'number'
|| typeof aspectRatio !== 'number'
|| typeof near !== 'number'
|| typeof far !== 'number') {
throw new TypeError('Matrix4.createPerspective expects numeric arguments.');
}
if (near <= 0 || far <= near) {
throw new RangeError('Matrix4.createPerspective expects 0 < near < far.');
}
const out = Matrix4.#createEmpty();
const projectionScale = PROJECTION_SCALE_NUMERATOR / Math.tan(fieldOfViewRadians / HALF_FIELD_OF_VIEW_DIVISOR);
const inverseDepthRange = DEPTH_RANGE_NUMERATOR / (near - far);
// Column-major layout:
out[0] = projectionScale / aspectRatio;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = projectionScale;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = (far + near) * inverseDepthRange;
out[11] = PERSPECTIVE_W_COMPONENT_SCALE;
out[12] = 0;
out[13] = 0;
out[14] = (PERSPECTIVE_Z_RANGE_MULTIPLIER * far * near) * inverseDepthRange;
out[15] = 0;
return out;
}
/**
* Creates a translation matrix.
*
* @param {number} translateX - Translation along X axis.
* @param {number} translateY - Translation along Y axis.
* @param {number} translateZ - Translation along Z axis.
* @returns {Float32Array} - A new translation matrix.
*/
static createTranslation(translateX, translateY, translateZ) {
if (typeof translateX !== 'number'
|| typeof translateY !== 'number'
|| typeof translateZ !== 'number') {
throw new TypeError('Matrix4.createTranslation expects numeric arguments.');
}
const out = Matrix4.createIdentity();
out[12] = translateX;
out[13] = translateY;
out[14] = translateZ;
return out;
}
/**
* Creates a rotation matrix around the X axis.
*
* @param {number} angleRadians - Angle in radians.
* @returns {Float32Array} - A new rotation matrix.
*/
static createRotationX(angleRadians) {
if (typeof angleRadians !== 'number') {
throw new TypeError('Matrix4.createRotationX expects a numeric argument.');
}
const cosAngle = Math.cos(angleRadians);
const sinAngle = Math.sin(angleRadians);
const out = Matrix4.#createEmpty();
out[0] = 1;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = cosAngle;
out[6] = sinAngle;
out[7] = 0;
out[8] = 0;
out[9] = -sinAngle;
out[10] = cosAngle;
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;
return out;
}
/**
* Creates a rotation matrix around the Y axis.
*
* @param {number} angleRadians - Angle in radians.
* @returns {Float32Array} - A new rotation matrix.
*/
static createRotationY(angleRadians) {
if (typeof angleRadians !== 'number') {
throw new TypeError('Matrix4.createRotationY expects a numeric argument.');
}
const cosAngle = Math.cos(angleRadians);
const sinAngle = Math.sin(angleRadians);
const out = Matrix4.#createEmpty();
out[0] = cosAngle;
out[1] = 0;
out[2] = -sinAngle;
out[3] = 0;
out[4] = 0;
out[5] = 1;
out[6] = 0;
out[7] = 0;
out[8] = sinAngle;
out[9] = 0;
out[10] = cosAngle;
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;
return out;
}
/**
* Creates a rotation matrix around the Z axis.
*
* @param {number} angleRadians - Angle in radians.
* @returns {Float32Array} - A new rotation matrix.
*/
static createRotationZ(angleRadians) {
if (typeof angleRadians !== 'number') {
throw new TypeError('Matrix4.createRotationZ expects a numeric argument.');
}
const cosAngle = Math.cos(angleRadians);
const sinAngle = Math.sin(angleRadians);
const out = Matrix4.#createEmpty();
out[0] = cosAngle;
out[1] = sinAngle;
out[2] = 0;
out[3] = 0;
out[4] = -sinAngle;
out[5] = cosAngle;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = 1;
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;
return out;
}
/**
* Multiplies two 4x4 matrices: result = leftMatrix * rightMatrix.
*
* @param {Float32Array} leftMatrix - Left-hand matrix (4x4).
* @param {Float32Array} rightMatrix - Right-hand matrix (4x4).
* @returns {Float32Array} - A new matrix containing the product.
*/
static multiply(leftMatrix, rightMatrix) {
if (!(leftMatrix instanceof Float32Array)
|| leftMatrix.length !== MATRIX_4x4_ELEMENT_COUNT
|| !(rightMatrix instanceof Float32Array)
|| rightMatrix.length !== MATRIX_4x4_ELEMENT_COUNT) {
throw new TypeError('Matrix4.multiply expects two 4x4 Float32Array matrices.');
}
const out = Matrix4.#createEmpty();
return Matrix4.#multiplyIntoUnchecked(out, leftMatrix, rightMatrix);
}
/**
* Multiplies two 4x4 matrices into an existing output matrix:
* out = leftMatrix * rightMatrix.
*
* Notes: out must not be the same object as leftMatrix or rightMatrix.
*
* @param {Float32Array} out - Output 4x4 matrix.
* @param {Float32Array} leftMatrix - Left-hand matrix (4x4).
* @param {Float32Array} rightMatrix - Right-hand matrix (4x4).
* @returns {Float32Array} - The output matrix (out).
*/
static multiplyTo(out, leftMatrix, rightMatrix) {
if (!(out instanceof Float32Array)
|| out.length !== MATRIX_4x4_ELEMENT_COUNT
|| !(leftMatrix instanceof Float32Array)
|| leftMatrix.length !== MATRIX_4x4_ELEMENT_COUNT
|| !(rightMatrix instanceof Float32Array)
|| rightMatrix.length !== MATRIX_4x4_ELEMENT_COUNT) {
throw new TypeError('Matrix4.multiplyTo expects three 4x4 Float32Array matrices.');
}
if (out === leftMatrix || out === rightMatrix) {
throw new Error('Matrix4.multiplyTo does not support in-place multiplication. Use a separate output matrix.');
}
return Matrix4.#multiplyIntoUnchecked(out, leftMatrix, rightMatrix);
}
/**
* Multiplies several matrices in sequence:
* result = m0 * m1 * m2 * ... * mn
*
* Notes: If no matrices are provided, returns a new identity matrix. If exactly one matrix is provided, returns the same matrix instance (no copy).
*
* @param {...Float32Array} matrices - Matrices to multiply, in order.
* @returns {Float32Array} - The resulting matrix.
*/
static multiplyMany(...matrices) {
if (matrices.length === 0) {
return Matrix4.createIdentity();
}
let result = matrices[0];
for (let index = 1; index < matrices.length; index += 1) {
result = Matrix4.multiply(result, matrices[index]);
}
return result;
}
/**
* Transposes a 4x4 matrix.
*
* @param {Float32Array} matrix - Input 4x4 matrix.
* @returns {Float32Array} - A new transposed matrix.
*/
static transpose(matrix) {
if (!(matrix instanceof Float32Array) || matrix.length !== MATRIX_4x4_ELEMENT_COUNT) {
throw new TypeError('`Matrix4.transpose` expects a 4x4 `Float32Array` matrix.');
}
const out = Matrix4.#createEmpty();
return Matrix4.#transposeIntoUnchecked(out, matrix);
}
/**
* Transposes a 4x4 matrix into an existing output matrix.
*
* Notes: out must not be the same object as matrix.
*
* @param {Float32Array} out - Output 4x4 matrix.
* @param {Float32Array} matrix - Input 4x4 matrix.
* @returns {Float32Array} - The output matrix (out).
*/
static transposeTo(out, matrix) {
if (!(out instanceof Float32Array)
|| out.length !== MATRIX_4x4_ELEMENT_COUNT
|| !(matrix instanceof Float32Array)
|| matrix.length !== MATRIX_4x4_ELEMENT_COUNT) {
throw new TypeError('`Matrix4.transposeTo` expects two 4x4 `Float32Array` matrices.');
}
if (out === matrix) {
throw new Error('`Matrix4.transposeTo` does not support in-place transpose. Use a separate output matrix.');
}
return Matrix4.#transposeIntoUnchecked(out, matrix);
}
/**
* Inverts a 4x4 matrix.
*
* @param {Float32Array} matrix - Input 4x4 matrix.
* @returns {Float32Array} - A new inverted matrix.
*/
static invert(matrix) {
if (!(matrix instanceof Float32Array) || matrix.length !== MATRIX_4x4_ELEMENT_COUNT) {
throw new TypeError('`Matrix4.invert` expects a 4x4 `Float32Array` matrix.');
}
const out = Matrix4.#createEmpty();
return Matrix4.#invertIntoUnchecked(out, matrix);
}
/**
* Inverts a 4x4 matrix into an existing output matrix.
*
* @param {Float32Array} out - Output 4x4 matrix.
* @param {Float32Array} matrix - Input 4x4 matrix.
* @returns {Float32Array} - The output matrix (out).
*/
static invertTo(out, matrix) {
if (!(out instanceof Float32Array)
|| out.length !== MATRIX_4x4_ELEMENT_COUNT
|| !(matrix instanceof Float32Array)
|| matrix.length !== MATRIX_4x4_ELEMENT_COUNT) {
throw new TypeError('`Matrix4.invertTo` expects two 4x4 `Float32Array` matrices.');
}
if (out === matrix) {
throw new Error('`Matrix4.invertTo` does not support in-place inversion.');
}
return Matrix4.#invertIntoUnchecked(out, matrix);
}
/**
* Multiplies two 4x4 matrices into out without validation.
*
* @param {Float32Array} out - Output 4x4 matrix that will receive the result.
* @param {Float32Array} leftMatrix - Left-hand 4x4 matrix.
* @param {Float32Array} rightMatrix - Right-hand 4x4 matrix.
* @returns {Float32Array} - The output matrix (out).
* @private
*/
static #multiplyIntoUnchecked(out, leftMatrix, rightMatrix) {
for (let columnIndex = 0; columnIndex < MATRIX_COLUMN_COUNT; columnIndex += 1) {
const rightColumnOffset = columnIndex * MATRIX_STRIDE;
for (let rowIndex = 0; rowIndex < MATRIX_ROW_COUNT; rowIndex += 1) {
const resultIndex = rightColumnOffset + rowIndex;
out[resultIndex] =
leftMatrix[0 * MATRIX_STRIDE + rowIndex] * rightMatrix[rightColumnOffset + 0]
+ leftMatrix[1 * MATRIX_STRIDE + rowIndex] * rightMatrix[rightColumnOffset + 1]
+ leftMatrix[2 * MATRIX_STRIDE + rowIndex] * rightMatrix[rightColumnOffset + 2]
+ leftMatrix[3 * MATRIX_STRIDE + rowIndex] * rightMatrix[rightColumnOffset + 3];
}
}
return out;
}
/**
* Transposes a 4x4 matrix into out without validation.
*
* @param {Float32Array} out - Output 4x4 matrix, that will receive the result.
* @param {Float32Array} matrix - Input 4x4 matrix.
* @returns {Float32Array} - The output matrix (out).
* @private
*/
static #transposeIntoUnchecked(out, matrix) {
out[0] = matrix[0];
out[1] = matrix[4];
out[2] = matrix[8];
out[3] = matrix[12];
out[4] = matrix[1];
out[5] = matrix[5];
out[6] = matrix[9];
out[7] = matrix[13];
out[8] = matrix[2];
out[9] = matrix[6];
out[10] = matrix[10];
out[11] = matrix[14];
out[12] = matrix[3];
out[13] = matrix[7];
out[14] = matrix[11];
out[15] = matrix[15];
return out;
}
/**
* Inverts a 4x4 matrix into out without validation. Throws when the matrix is not invertible.
*
* @param {Float32Array} out - Output 4x4 matrix, that will receive the result.
* @param {Float32Array} matrix - Input 4x4 matrix.
* @returns {Float32Array} - The output matrix (out).
* @private
*/
static #invertIntoUnchecked(out, matrix) {
const a00 = matrix[0];
const a01 = matrix[1];
const a02 = matrix[2];
const a03 = matrix[3];
const a10 = matrix[4];
const a11 = matrix[5];
const a12 = matrix[6];
const a13 = matrix[7];
const a20 = matrix[8];
const a21 = matrix[9];
const a22 = matrix[10];
const a23 = matrix[11];
const a30 = matrix[12];
const a31 = matrix[13];
const a32 = matrix[14];
const a33 = matrix[15];
const b00 = a00 * a11 - a01 * a10;
const b01 = a00 * a12 - a02 * a10;
const b02 = a00 * a13 - a03 * a10;
const b03 = a01 * a12 - a02 * a11;
const b04 = a01 * a13 - a03 * a11;
const b05 = a02 * a13 - a03 * a12;
const b06 = a20 * a31 - a21 * a30;
const b07 = a20 * a32 - a22 * a30;
const b08 = a20 * a33 - a23 * a30;
const b09 = a21 * a32 - a22 * a31;
const b10 = a21 * a33 - a23 * a31;
const b11 = a22 * a33 - a23 * a32;
const determinant =
b00 * b11
- b01 * b10
+ b02 * b09
+ b03 * b08
- b04 * b07
+ b05 * b06;
if (Math.abs(determinant) < MIN_INVERTIBLE_DETERMINANT_ABS) {
throw new Error('`Matrix4.invertTo`: matrix is not invertible.');
}
const inverseDeterminant = INVERSE_DETERMINANT_NUMERATOR / determinant;
out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * inverseDeterminant;
out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * inverseDeterminant;
out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * inverseDeterminant;
out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * inverseDeterminant;
out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * inverseDeterminant;
out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * inverseDeterminant;
out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * inverseDeterminant;
out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * inverseDeterminant;
out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * inverseDeterminant;
out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * inverseDeterminant;
out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * inverseDeterminant;
out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * inverseDeterminant;
out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * inverseDeterminant;
out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * inverseDeterminant;
out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * inverseDeterminant;
out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * inverseDeterminant;
return out;
}
/**
* Internal helper to create a zero-filled 4x4 matrix.
*
* @returns {Float32Array} - A new zero-filled 4x4 matrix (length 16).
* @private
*/
static #createEmpty() {
return new Float32Array(MATRIX_4x4_ELEMENT_COUNT);
}
}