import { Vector4 } from './Vector4'; const EPSILON = 1e-6; import { Vector3 } from './Vector3'; import { Matrix3 } from './Matrix3'; export class Matrix4 { public static readonly identity: Matrix4 = new Matrix4().setIdentity(); private readonly values: Float32Array; public constructor(values: number[] | null = null) { this.values = new Float32Array(16); if (values) { this.init(values); } else { this.setIdentity(); } } public static frustum( left: number, right: number, bottom: number, top: number, near: number, far: number ): Matrix4 { const rl: number = right - left; const tb: number = top - bottom; const fn: number = far - near; return new Matrix4([ (near * 2) / rl, 0, 0, 0, 0, (near * 2) / tb, 0, 0, (right + left) / rl, (top + bottom) / tb, -(far + near) / fn, -1, 0, 0, -(far * near * 2) / fn, 0 ]); } public static perspective( fov: number, aspect: number, near: number, far: number ): Matrix4 { const top: number = near * Math.tan((fov * Math.PI) / 360.0); const right: number = top * aspect; return Matrix4.frustum(-right, right, -top, top, near, far); } public static orthographic( left: number, right: number, bottom: number, top: number, near: number, far: number ): Matrix4 { const rl: number = right - left; const tb: number = top - bottom; const fn: number = far - near; return new Matrix4([ 2 / rl, 0, 0, 0, 0, 2 / tb, 0, 0, 0, 0, -2 / fn, 0, -(left + right) / rl, -(top + bottom) / tb, -(far + near) / fn, 1 ]); } public static lookAt( position: Vector3, target: Vector3, up: Vector3 = Vector3.up ): Matrix4 { if (position.equals(target)) { return this.identity.copy(); } const z: Vector3 = position.difference(target).normalize(); const x: Vector3 = up.cross(z).normalize(); const y: Vector3 = z.cross(x).normalize(); return new Matrix4([ x.x, y.x, z.x, 0, x.y, y.y, z.y, 0, x.z, y.z, z.z, 0, -x.dot(position), -y.dot(position), -z.dot(position), 1 ]); } public static product(m1: Matrix4, m2: Matrix4, result: Matrix4 | null = null): Matrix4 { const a: Float32Array = m1.values; const b: Float32Array = m2.values; const productValues: number[] = [ b[0] * a[0] + b[1] * a[4] + b[2] * a[8] + b[3] * a[12], b[0] * a[1] + b[1] * a[5] + b[2] * a[9] + b[3] * a[13], b[0] * a[2] + b[1] * a[6] + b[2] * a[10] + b[3] * a[14], b[0] * a[3] + b[1] * a[7] + b[2] * a[11] + b[3] * a[15], b[4] * a[0] + b[5] * a[4] + b[6] * a[8] + b[7] * a[12], b[4] * a[1] + b[5] * a[5] + b[6] * a[9] + b[7] * a[13], b[4] * a[2] + b[5] * a[6] + b[6] * a[10] + b[7] * a[14], b[4] * a[3] + b[5] * a[7] + b[6] * a[11] + b[7] * a[15], b[8] * a[0] + b[9] * a[4] + b[10] * a[8] + b[11] * a[12], b[8] * a[1] + b[9] * a[5] + b[10] * a[9] + b[11] * a[13], b[8] * a[2] + b[9] * a[6] + b[10] * a[10] + b[11] * a[14], b[8] * a[3] + b[9] * a[7] + b[10] * a[11] + b[11] * a[15], b[12] * a[0] + b[13] * a[4] + b[14] * a[8] + b[15] * a[12], b[12] * a[1] + b[13] * a[5] + b[14] * a[9] + b[15] * a[13], b[12] * a[2] + b[13] * a[6] + b[14] * a[10] + b[15] * a[14], b[12] * a[3] + b[13] * a[7] + b[14] * a[11] + b[15] * a[15], ]; if (result) { result.init(productValues); return result; } else { return new Matrix4(productValues); } } public at(index: number): number { return this.values[index]; } public init(values: unknown[]): this { if (values.length !== 16) { throw new Error('Initialization array must have exactly 16 elements.'); } for (const val of values) { if (typeof val !== 'number') { throw new Error('Array contains non-numbers'); } } let i = 0; for (const value of values) { this.values[i++] = Number(value); } return this; } public reset(): void { for (let i = 0; i < this.values.length; i++) { this.values[i] = 0; } } public copy(dest: Matrix4 | null = null): Matrix4 { const destination: Matrix4 = dest ?? new Matrix4(); for (let i = 0; i < this.values.length; i++) { destination.values[i] = this.values[i]; } return destination; } public all(): number[] { const data: number[] = []; for (const value of this.values) { data.push(value); } return data; } public row(index: number): number[] { if (index < 0 || index > 3) { throw new RangeError('Row index must be between 0 and 3.'); } return [ this.values[index * 4], this.values[index * 4 + 1], this.values[index * 4 + 2], this.values[index * 4 + 3] ]; } public col(index: number): number[] { if (index < 0 || index > 3) { throw new RangeError('Column index must be between 0 and 3.'); } return [ this.values[index], this.values[index + 4], this.values[index + 8], this.values[index + 12] ]; } public equals(matrix: Matrix4, threshold: number = EPSILON): boolean { for (let i = 0; i < this.values.length; i++) { if (Math.abs(this.values[i] - matrix.at(i)) > threshold) { return false; } } return true; } public determinant(): number { const m = this.values; const det00 = m[0] * m[5] - m[1] * m[4]; const det01 = m[0] * m[6] - m[2] * m[4]; const det02 = m[0] * m[7] - m[3] * m[4]; const det03 = m[1] * m[6] - m[2] * m[5]; const det04 = m[1] * m[7] - m[3] * m[5]; const det05 = m[2] * m[7] - m[3] * m[6]; const det06 = m[4] * m[9] - m[5] * m[8]; const det07 = m[4] * m[10] - m[6] * m[8]; const det08 = m[4] * m[11] - m[7] * m[8]; const det09 = m[5] * m[10] - m[6] * m[9]; const det10 = m[5] * m[11] - m[7] * m[9]; const det11 = m[6] * m[11] - m[7] * m[10]; return ( det00 * det11 - det01 * det10 + det02 * det09 + det03 * det08 - det04 * det07 + det05 * det06 ); } public setIdentity(): this { this.reset(); this.values[0] = 1; this.values[5] = 1; this.values[10] = 1; this.values[15] = 1; return this; } public transpose(): Matrix4 { const m: Float32Array = this.values; const transposed: Float32Array = new Float32Array(16); for (let row = 0; row < 4; row++) { for (let col = 0; col < 4; col++) { transposed[col * 4 + row] = m[row * 4 + col]; } } return new Matrix4(Array.from(transposed)); } public inverse(): Matrix4 | null { const m = this.values; const inv: number[] = new Array(16); inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10]; inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10]; inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9]; inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9]; inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10]; inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10]; inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9]; inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9]; inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6]; inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6]; inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5]; inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5]; inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6]; inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6]; inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5]; inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5]; const det: number = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]; if (Math.abs(det) < EPSILON) { return null; } const inverseDet: number = 1.0 / det; for (let i = 0; i < 16; i++) { inv[i] *= inverseDet; } return new Matrix4(inv); } public multiply(matrix: Matrix4): Matrix4 { return Matrix4.product(this, matrix); } public multiplyVector3(vector: Vector3): Vector3 { const x: number = vector.x; const y: number = vector.y; const z: number = vector.z; const m: Float32Array = this.values; return new Vector3([ m[0] * x + m[4] * y + m[8] * z + m[12], m[1] * x + m[5] * y + m[9] * z + m[13], m[2] * x + m[6] * y + m[10] * z + m[14] ]); } public multiplyVector4(vector: Vector4): Vector4 { const m: Float32Array = this.values; const x: number = vector.x; const y: number = vector.y; const z: number = vector.z; const w: number = vector.w; return new Vector4([ m[0] * x + m[4] * y + m[8] * z + m[12] * w, m[1] * x + m[5] * y + m[9] * z + m[13] * w, m[2] * x + m[6] * y + m[10] * z + m[14] * w, m[3] * x + m[7] * y + m[11] * z + m[15] * w ]); } public toMatrix3(): Matrix3 { return new Matrix3([ this.values[0], this.values[1], this.values[2], this.values[4], this.values[5], this.values[6], this.values[8], this.values[9], this.values[10] ]); } public toInverseMatrix3(): Matrix3 | null { const matrix3 = this.toMatrix3(); return matrix3.inverse(); } public translate(vector: Vector3): Matrix4 { const translation = new Matrix4([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, vector.x, vector.y, vector.z, 1 ]); return Matrix4.product(this, translation); } public scale(vector: Vector3): Matrix4 { const scaling = new Matrix4([ vector.x, 0, 0, 0, 0, vector.y, 0, 0, 0, 0, vector.z, 0, 0, 0, 0, 1 ]); return Matrix4.product(this, scaling); } public rotate(angle: number, axis: Vector3): Matrix4 | null { const x: number = axis.x; const y: number = axis.y; const z: number = axis.z; const length: number = Math.sqrt(x * x + y * y + z * z); if (length === 0) { return null; } if (length !== 1) { const invLength: number = 1 / length; this.rotate(angle, new Vector3([x * invLength, y * invLength, z * invLength])); return this; } const s: number = Math.sin(angle); const c: number = Math.cos(angle); const t: number = 1 - c; const m: number[] = [ x * x * t + c, y * x * t + z * s, z * x * t - y * s, 0, x * y * t - z * s, y * y * t + c, z * y * t + x * s, 0, x * z * t + y * s, y * z * t - x * s, z * z * t + c, 0, 0, 0, 0, 1 ]; const rotationMatrix = new Matrix4(m); return Matrix4.product(this, rotationMatrix); } public toArray(): number[] { return Array.from(this.values); } }