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:
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));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.
| 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).
<!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>