Math Library Comparison

Comparing gl-matrix, Three.js Math and twgl's Math Module

Three.js 30,815 (Three.js) has classes, Vector3, Matrix4, Quaternion and Euler, whose methods mutate this and chain; Matrix4.elements is column-major, but Matrix4.set() takes row-major arguments. twgl.js 3,005 (twgl.js) has m4 and v3 functions that return new arrays unless given a last dst argument. The listing computes the shelf camera's view-projection in all three:

The shelf camera's view-projection matrix in three librariesJavaScript
import { mat4 } from 'https://cdn.jsdelivr.net/npm/gl-matrix@3.4.4/esm/index.js';
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js';
import { m4 } from 'https://cdn.jsdelivr.net/npm/twgl.js@7.0.0/dist/7.x/twgl-full.module.js';
const eye = [1.1, 0.9, 2.9], target = [0.1, -0.2, 0], fovy = 40 * Math.PI / 180;
const gl = mat4.multiply(mat4.create(),                 // gl-matrix: out-parameter functions
  mat4.perspective(mat4.create(), fovy, 620 / 220, 0.1, 20),
  mat4.lookAt(mat4.create(), eye, target, [0, 1, 0]));
const camera = new THREE.PerspectiveCamera(40, 620 / 220, 0.1, 20);   // three.js: objects
camera.position.set(...eye); camera.lookAt(...target); camera.updateMatrixWorld();
const three = camera.projectionMatrix.clone().multiply(camera.matrixWorldInverse).elements;
const tw = m4.multiply(m4.perspective(fovy, 620 / 220, 0.1, 20),   // twgl: returns new arrays
  m4.inverse(m4.lookAt(eye, target, [0, 1, 0])));               // lookAt = camera matrix
const diff = (a) => Math.max(...[...a].map((v, i) => Math.abs(v - gl[i]))).toExponential(1);
console.log('three.js', THREE.REVISION, diff(three), '| twgl', diff(tw));
Output
three.js 186 1.6e-7 | twgl 6.0e-8

They agree to within float rounding, past one trap: twgl's m4.lookAt() returns the camera's own matrix (like gl-matrix 5,695 's targetTo), so the view matrix is its inverse; without it the result is off by 6.2.

Three JavaScript matrix libraries compared
Library Style Allocation Licence, version Best for
gl-matrix Functions, output first Only on create() MIT, 3.4.4 Raw WebGL, engines
Three.js math Classes, chaining On new or clone() MIT, r186 Three.js scenes
twgl.js m4, v3 Functions, new result Unless dst is given MIT, 7.0.0 twgl programs

Inside Three.js use its own classes; in raw WebGL, gl-matrix; for WebGPU, gl-matrix's ZO functions or wgpu-matrix 3.4.2 481 , by twgl's author (WebGPU).

The shelf drawn three times with view-projection matrices from gl-matrix, Three.js and twgl.jsHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; }
  .names div { flex: 1; padding: 4px 2px; }
  .names b { display: block; font-size: 12px; }
</style>
<canvas id="view" width="600" height="190"></canvas>
<div class="names" id="names"></div>
<script type="module">
import { mat4 } from 'https://cdn.jsdelivr.net/npm/gl-matrix@3.4.4/esm/index.js';
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js';
import { m4 } from 'https://cdn.jsdelivr.net/npm/twgl.js@7.0.0/dist/7.x/twgl-full.module.js';

const eye = [1.7, 1.3, 4.4], target = [0.1, -0.2, 0], fovy = 40 * Math.PI / 180, aspect = 196 / 190;
// gl-matrix: out-parameter functions
const viaGlMatrix = mat4.multiply(mat4.create(),
  mat4.perspective(mat4.create(), fovy, aspect, 0.1, 20), mat4.lookAt(mat4.create(), eye, target, [0, 1, 0]));
// Three.js: objects whose methods mutate and chain
const camera = new THREE.PerspectiveCamera(40, aspect, 0.1, 20);
camera.position.set(...eye); camera.lookAt(...target); camera.updateMatrixWorld();
const viaThree = camera.projectionMatrix.clone().multiply(camera.matrixWorldInverse).elements;
// twgl: functions returning new arrays; its lookAt is the CAMERA matrix, so invert it
const viaTwgl = m4.multiply(m4.perspective(fovy, aspect, 0.1, 20), m4.inverse(m4.lookAt(eye, target, [0, 1, 0])));
const diff = (a) => Math.max(...[...a].map((v, i) => Math.abs(v - viaGlMatrix[i]))).toExponential(1);

// A shelf of six covers (unit quads) placed with gl-matrix model matrices.
const gl = document.getElementById('view').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform mat4 uViewProjection, uModel; out vec2 vUV;
void main() {
  vUV = vec2[6](vec2(0), vec2(1, 0), vec2(1), vec2(0), vec2(1), vec2(0, 1))[gl_VertexID];
  gl_Position = uViewProjection * uModel * vec4(vUV - vec2(0.5, 0.0), 0.0, 1.0);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; uniform vec3 uColor; in vec2 vUV; out vec4 c;
void main() { c = vec4(uColor * mix(0.7, 1.15, vUV.y) * (vUV.x < 0.1 ? 0.6 : 1.0), 1.0); }`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
gl.bindVertexArray(gl.createVertexArray());
const u = (n) => gl.getUniformLocation(program, n);
const colors = [[0.12, 0.37, 0.55], [0.36, 0.25, 0.6], [0.88, 0.6, 0.06], [0.25, 0.49, 0.23], [0.71, 0.27, 0.18], [0.16, 0.62, 0.56]];
const model = mat4.create();
gl.enable(gl.SCISSOR_TEST);
[[viaGlMatrix, 'gl-matrix 3.4.4', 'reference'], [viaThree, `Three.js r${THREE.REVISION}`, 'max diff ' + diff(viaThree)],
 [viaTwgl, 'twgl.js 7.0.0', 'max diff ' + diff(viaTwgl)]].forEach(([vp, name, note], i) => {
  gl.viewport(i * 202, 0, 196, 190); gl.scissor(i * 202, 0, 196, 190);
  gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT);
  gl.uniformMatrix4fv(u('uViewProjection'), false, vp);
  mat4.fromTranslation(model, [0, -0.5, 0]); mat4.rotateX(model, model, -Math.PI / 2);   // the plank
  mat4.scale(model, model, [3.4, 1, 1]); mat4.translate(model, model, [0, -0.5, 0]);
  gl.uniformMatrix4fv(u('uModel'), false, model); gl.uniform3fv(u('uColor'), [0.54, 0.38, 0.25]);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  colors.forEach((color, k) => {
    mat4.fromTranslation(model, [-1.3 + k * 0.52, -0.5, 0]);
    mat4.rotateY(model, model, (30 - k * 12) * Math.PI / 180);
    mat4.scale(model, model, [0.42, 0.98, 1]);
    gl.uniformMatrix4fv(u('uModel'), false, model); gl.uniform3fv(u('uColor'), color);
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  });
  document.getElementById('names').insertAdjacentHTML('beforeend', `<div><b>${name}</b>${note}</div>`);
});
</script>