UNPKG

shaku

Version:

A simple and effective JavaScript game development framework that knows its place!

550 lines (490 loc) 19.2 kB
/** * Matrix class. * Based on code from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web * * |-- copyright and license --| * @module Shaku * @file shaku\src\utils\matrix.js * @author Ronen Ness (ronenness@gmail.com | http://ronenness.com) * @copyright (c) 2021 Ronen Ness * @license MIT * |-- end copyright and license --| * */ 'use strict'; const Vertex = require("../gfx/vertex"); const Vector2 = require("./vector2"); const Vector3 = require("./vector3"); const EPSILON = Number.EPSILON; /** * Implements a matrix. */ class Matrix { /** * Create the matrix. * @param values matrix values array. * @param cloneValues if true or undefined, will clone values instead of just holding a reference to them. */ constructor(values, cloneValues) { // if no values are set, use identity if (!values) { values = Matrix.identity.values; cloneValues = true; } // clone values if (cloneValues || cloneValues === undefined) { this.values = values.slice(0); } // copy values reference else { this.values = values; } } /** * Set the matrix values. */ set(v11, v12, v13, v14, v21, v22, v23, v24, v31, v32, v33, v34, v41, v42, v43, v44) { this.values = new Float32Array([ v11, v12, v13, v14, v21, v22, v23, v24, v31, v32, v33, v34, v41, v42, v43, v44 ]); } /** * Clone the matrix. * @returns {Matrix} Cloned matrix. */ clone() { let ret = new Matrix(this.values, true); return ret; } /** * Compare this matrix to another matrix. * @param {Matrix} other Matrix to compare to. * @returns {Boolean} If matrices are the same. */ equals(other) { if (other === this) { return true; } if (!other) { return false; } for (let i = 0; i < this.values.length; ++i) { if (this.values[i] !== other.values[i]) { return false; } } return true; } /** * Clone and invert the matrix. * @returns {Matrix} Clonsed inverted matrix. */ inverted() { return this.clone().invertSelf(); } /** * Invert this matrix. * @returns {Matrix} Self. */ invertSelf() { // based on http://www.euclideanspace.com/maths/algebra/matrix/functions/inverse/fourD/index.htm const te = this.values, n11 = te[ 0 ], n21 = te[ 1 ], n31 = te[ 2 ], n41 = te[ 3 ], n12 = te[ 4 ], n22 = te[ 5 ], n32 = te[ 6 ], n42 = te[ 7 ], n13 = te[ 8 ], n23 = te[ 9 ], n33 = te[ 10 ], n43 = te[ 11 ], n14 = te[ 12 ], n24 = te[ 13 ], n34 = te[ 14 ], n44 = te[ 15 ], t11 = n23 * n34 * n42 - n24 * n33 * n42 + n24 * n32 * n43 - n22 * n34 * n43 - n23 * n32 * n44 + n22 * n33 * n44, t12 = n14 * n33 * n42 - n13 * n34 * n42 - n14 * n32 * n43 + n12 * n34 * n43 + n13 * n32 * n44 - n12 * n33 * n44, t13 = n13 * n24 * n42 - n14 * n23 * n42 + n14 * n22 * n43 - n12 * n24 * n43 - n13 * n22 * n44 + n12 * n23 * n44, t14 = n14 * n23 * n32 - n13 * n24 * n32 - n14 * n22 * n33 + n12 * n24 * n33 + n13 * n22 * n34 - n12 * n23 * n34; const det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14; if ( det === 0 ) return this.set( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ); const detInv = 1 / det; te[ 0 ] = t11 * detInv; te[ 1 ] = ( n24 * n33 * n41 - n23 * n34 * n41 - n24 * n31 * n43 + n21 * n34 * n43 + n23 * n31 * n44 - n21 * n33 * n44 ) * detInv; te[ 2 ] = ( n22 * n34 * n41 - n24 * n32 * n41 + n24 * n31 * n42 - n21 * n34 * n42 - n22 * n31 * n44 + n21 * n32 * n44 ) * detInv; te[ 3 ] = ( n23 * n32 * n41 - n22 * n33 * n41 - n23 * n31 * n42 + n21 * n33 * n42 + n22 * n31 * n43 - n21 * n32 * n43 ) * detInv; te[ 4 ] = t12 * detInv; te[ 5 ] = ( n13 * n34 * n41 - n14 * n33 * n41 + n14 * n31 * n43 - n11 * n34 * n43 - n13 * n31 * n44 + n11 * n33 * n44 ) * detInv; te[ 6 ] = ( n14 * n32 * n41 - n12 * n34 * n41 - n14 * n31 * n42 + n11 * n34 * n42 + n12 * n31 * n44 - n11 * n32 * n44 ) * detInv; te[ 7 ] = ( n12 * n33 * n41 - n13 * n32 * n41 + n13 * n31 * n42 - n11 * n33 * n42 - n12 * n31 * n43 + n11 * n32 * n43 ) * detInv; te[ 8 ] = t13 * detInv; te[ 9 ] = ( n14 * n23 * n41 - n13 * n24 * n41 - n14 * n21 * n43 + n11 * n24 * n43 + n13 * n21 * n44 - n11 * n23 * n44 ) * detInv; te[ 10 ] = ( n12 * n24 * n41 - n14 * n22 * n41 + n14 * n21 * n42 - n11 * n24 * n42 - n12 * n21 * n44 + n11 * n22 * n44 ) * detInv; te[ 11 ] = ( n13 * n22 * n41 - n12 * n23 * n41 - n13 * n21 * n42 + n11 * n23 * n42 + n12 * n21 * n43 - n11 * n22 * n43 ) * detInv; te[ 12 ] = t14 * detInv; te[ 13 ] = ( n13 * n24 * n31 - n14 * n23 * n31 + n14 * n21 * n33 - n11 * n24 * n33 - n13 * n21 * n34 + n11 * n23 * n34 ) * detInv; te[ 14 ] = ( n14 * n22 * n31 - n12 * n24 * n31 - n14 * n21 * n32 + n11 * n24 * n32 + n12 * n21 * n34 - n11 * n22 * n34 ) * detInv; te[ 15 ] = ( n12 * n23 * n31 - n13 * n22 * n31 + n13 * n21 * n32 - n11 * n23 * n32 - n12 * n21 * n33 + n11 * n22 * n33 ) * detInv; return this; } /** * Transform a target. * @param {Vector2|Vector3|Vertex} target Transforms a target, that can be vector2, vector3, or vertex. * @returns {Vector2|Vector3|Vector3} Transformed result. */ transform(target) { if (target.isVector2) { return Matrix.transformVector2(this, target); } if (target.isVector3) { return Matrix.transformVector3(this, target); } if (target.Vertex) { return Matrix.transformVertex(this, target); } throw new Error("Unknown type to transform!"); } /** * Multiply this matrix with another matrix, putting results in self. * @param {Matrix} other Matrix to multiply with. * @returns {Matrix} This. */ multiplySelfWith(other) { return Matrix.multiplyIntoFirst(this, other); } /** * Multiply this matrix with another matrix and return a new result matrix. * @param {Matrix} other Matrix to multiply with. * @returns {Matrix} New result matrix. */ multiplyWith(other) { return Matrix.multiply(this, other); } /** * Create an orthographic projection matrix. * @returns {Matrix} a new matrix with result. */ static createOrthographic(left, right, bottom, top, near, far) { return new Matrix([ 2 / (right - left), 0, 0, 0, 0, 2 / (top - bottom), 0, 0, 0, 0, 2 / (near - far), 0, (left + right) / (left - right), (bottom + top) / (bottom - top), (near + far) / (near - far), 1, ], false); } /** * Create a perspective projection matrix. * @returns {Matrix} a new matrix with result. */ static createPerspective(fieldOfViewInRadians, aspectRatio, near, far) { var f = 1.0 / Math.tan(fieldOfViewInRadians / 2); var rangeInv = 1 / (near - far); return new Matrix([ f / aspectRatio, 0, 0, 0, 0, f, 0, 0, 0, 0, (near + far) * rangeInv , -1, 0, 0, near * far * rangeInv * 2, 0 ], false); } /** * Create a translation matrix. * @returns {Matrix} a new matrix with result. */ static createTranslation(x, y, z) { return new Matrix([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x || 0, y || 0, z || 0, 1 ], false); } /** * Create a scale matrix. * @returns {Matrix} a new matrix with result. */ static createScale(x, y, z) { return new Matrix([ x || 1, 0, 0, 0, 0, y || 1, 0, 0, 0, 0, z || 1, 0, 0, 0, 0, 1 ], false); } /** * Create a rotation matrix around X axis. * @returns {Matrix} a new matrix with result. */ static createRotationX(a) { let sin = Math.sin; let cos = Math.cos; return new Matrix([ 1, 0, 0, 0, 0, cos(a), -sin(a), 0, 0, sin(a), cos(a), 0, 0, 0, 0, 1 ], false); } /** * Create a rotation matrix around Y axis. * @returns {Matrix} a new matrix with result. */ static createRotationY(a) { let sin = Math.sin; let cos = Math.cos; return new Matrix([ cos(a), 0, sin(a), 0, 0, 1, 0, 0, -sin(a), 0, cos(a), 0, 0, 0, 0, 1 ], false); } /** * Create a rotation matrix around Z axis. * @returns {Matrix} a new matrix with result. */ static createRotationZ(a) { let sin = Math.sin; let cos = Math.cos; return new Matrix([ cos(a), -sin(a), 0, 0, sin(a), cos(a), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ], false); } /** * Multiply two matrices. * @returns {Matrix} a new matrix with result. */ static multiply(matrixA, matrixB) { // Slice the second matrix up into rows let row0 = [matrixB.values[ 0], matrixB.values[ 1], matrixB.values[ 2], matrixB.values[ 3]]; let row1 = [matrixB.values[ 4], matrixB.values[ 5], matrixB.values[ 6], matrixB.values[ 7]]; let row2 = [matrixB.values[ 8], matrixB.values[ 9], matrixB.values[10], matrixB.values[11]]; let row3 = [matrixB.values[12], matrixB.values[13], matrixB.values[14], matrixB.values[15]]; // Multiply each row by matrixA let result0 = multiplyMatrixAndPoint(matrixA.values, row0); let result1 = multiplyMatrixAndPoint(matrixA.values, row1); let result2 = multiplyMatrixAndPoint(matrixA.values, row2); let result3 = multiplyMatrixAndPoint(matrixA.values, row3); // Turn the result rows back into a single matrix return new Matrix([ result0[0], result0[1], result0[2], result0[3], result1[0], result1[1], result1[2], result1[3], result2[0], result2[1], result2[2], result2[3], result3[0], result3[1], result3[2], result3[3] ], false); } /** * Creates a look-at matrix - a matrix rotated to look at a given position. * @param {Vector3} eyePosition Eye position. * @param {Vector3} targetPosition Position the matrix should look at. * @param {Vector3=} upVector Optional vector representing 'up' direction. * @returns {Matrix} a new matrix with result. */ static createLookAt(eyePosition, targetPosition, upVector) { const eye = eyePosition; const center = targetPosition; const up = upVector || Vector3.upReadonly; let x0, x1, x2, y0, y1, y2, z0, z1, z2, len; if ( Math.abs(eye.x - center.x) < EPSILON && Math.abs(eye.y - center.y) < EPSILON && Math.abs(eye.z - center.z) < EPSILON ) { return Matrix.identity.clone(); } z0 = eye.x - center.x; z1 = eye.y - center.y; z2 = eye.z - center.z; len = 1 / Math.hypot(z0, z1, z2); z0 *= len; z1 *= len; z2 *= len; x0 = up.y * z2 - up.z * z1; x1 = up.z * z0 - up.x * z2; x2 = up.x * z1 - up.y * z0; len = Math.hypot(x0, x1, x2); if (!len) { x0 = 0; x1 = 0; x2 = 0; } else { len = 1 / len; x0 *= len; x1 *= len; x2 *= len; } y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0; len = Math.hypot(y0, y1, y2); if (!len) { y0 = 0; y1 = 0; y2 = 0; } else { len = 1 / len; y0 *= len; y1 *= len; y2 *= len; } const out = []; out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0; out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0; out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0; out[12] = -(x0 * eye.x + x1 * eye.y + x2 * eye.z); out[13] = -(y0 * eye.x + y1 * eye.y + y2 * eye.z); out[14] = -(z0 * eye.x + z1 * eye.y + z2 * eye.z); out[15] = 1; return new Matrix(out, false); } /** * Multiply an array of matrices. * @param {Array<Matrix>} matrices Matrices to multiply. * @returns {Matrix} new matrix with multiply result. */ static multiplyMany(matrices) { let ret = matrices[0]; for(let i = 1; i < matrices.length; i++) { ret = Matrix.multiply(ret, matrices[i]); } return ret; } /** * Multiply two matrices and put result in first matrix. * @returns {Matrix} matrixA, after it was modified. */ static multiplyIntoFirst(matrixA, matrixB) { // Slice the second matrix up into rows let row0 = [matrixB.values[ 0], matrixB.values[ 1], matrixB.values[ 2], matrixB.values[ 3]]; let row1 = [matrixB.values[ 4], matrixB.values[ 5], matrixB.values[ 6], matrixB.values[ 7]]; let row2 = [matrixB.values[ 8], matrixB.values[ 9], matrixB.values[10], matrixB.values[11]]; let row3 = [matrixB.values[12], matrixB.values[13], matrixB.values[14], matrixB.values[15]]; // Multiply each row by matrixA let result0 = multiplyMatrixAndPoint(matrixA.values, row0); let result1 = multiplyMatrixAndPoint(matrixA.values, row1); let result2 = multiplyMatrixAndPoint(matrixA.values, row2); let result3 = multiplyMatrixAndPoint(matrixA.values, row3); // Turn the result rows back into a single matrix matrixA.set( result0[0], result0[1], result0[2], result0[3], result1[0], result1[1], result1[2], result1[3], result2[0], result2[1], result2[2], result2[3], result3[0], result3[1], result3[2], result3[3] ); // return the first matrix after it was modified return matrixA; } /** * Multiply an array of matrices into the first matrix in the array. * @param {Array<Matrix>} matrices Matrices to multiply. * @returns {Matrix} first matrix in array, after it was modified. */ static multiplyManyIntoFirst(matrices) { let ret = matrices[0]; for(let i = 1; i < matrices.length; i++) { ret = Matrix.multiplyIntoFirst(ret, matrices[i]); } return ret; } /** * Transform a 2d vertex. * @param {Matrix} matrix Matrix to use to transform vector. * @param {Vertex} vertex Vertex to transform. * @returns {Vertex} A transformed vertex (cloned, not the original). */ static transformVertex(matrix, vertex) { return new Vertex( (vertex.position.isVector2) ? Matrix.transformVector2(matrix, vertex.position) : Matrix.transformVector3(matrix, vertex.position), vertex.textureCoord, vertex.color); } /** * Transform a 2d vector. * @param {Matrix} matrix Matrix to use to transform vector. * @param {Vector2} vector Vector to transform. * @returns {Vector2} Transformed vector. */ static transformVector2(matrix, vector) { const x = vector.x, y = vector.y, z = vector.z || 0; const e = matrix.values; const w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] ); const resx = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w; const resy = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w; return new Vector2(resx, resy); } /** * Transform a 3d vector. * @param {Matrix} matrix Matrix to use to transform vector. * @param {Vector3} vector Vector to transform. * @returns {Vector3} Transformed vector. */ static transformVector3(matrix, vector) { const x = vector.x, y = vector.y, z = vector.z || 0; const e = matrix.values; const w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] ); const resx = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w; const resy = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w; const resz = ( e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] ) * w; return new Vector3(resx, resy, resz); } } /** * Multiply matrix and vector. * @private */ function multiplyMatrixAndPoint(matrix, point) { // Give a simple variable name to each part of the matrix, a column and row number let c0r0 = matrix[ 0], c1r0 = matrix[ 1], c2r0 = matrix[ 2], c3r0 = matrix[ 3]; let c0r1 = matrix[ 4], c1r1 = matrix[ 5], c2r1 = matrix[ 6], c3r1 = matrix[ 7]; let c0r2 = matrix[ 8], c1r2 = matrix[ 9], c2r2 = matrix[10], c3r2 = matrix[11]; let c0r3 = matrix[12], c1r3 = matrix[13], c2r3 = matrix[14], c3r3 = matrix[15]; // Now set some simple names for the point let x = point[0]; let y = point[1]; let z = point[2]; let w = point[3]; // Multiply the point against each part of the 1st column, then add together let resultX = (x * c0r0) + (y * c0r1) + (z * c0r2) + (w * c0r3); // Multiply the point against each part of the 2nd column, then add together let resultY = (x * c1r0) + (y * c1r1) + (z * c1r2) + (w * c1r3); // Multiply the point against each part of the 3rd column, then add together let resultZ = (x * c2r0) + (y * c2r1) + (z * c2r2) + (w * c2r3); // Multiply the point against each part of the 4th column, then add together let resultW = (x * c3r0) + (y * c3r1) + (z * c3r2) + (w * c3r3); return [resultX, resultY, resultZ, resultW]; } /** * An identity matrix. */ Matrix.identity = new Matrix([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ], false); Matrix.identity.isIdentity = true; Object.freeze(Matrix.identity); // export the matrix object module.exports = Matrix;