Every gl-matrix 5,695 operation takes the output first: vec3.add(out, a, b) writes a + b into out and returns it, so calls nest and nothing is allocated unless you call create(). Inputs may be plain arrays. The vector modules mirror GLSL's geometric functions (Common and Geometric Functions): add, subtract, scale, dot, cross, length, distance, normalize, lerp, and transformMat4() or transformQuat() to move a vector; vec4 repeats most of them for colors and homogeneous points. The listing computes quantities Directional and Point Lighting needs:
const { vec3 } = glMatrix;
const r = (v) => [...v].map((c) => +c.toFixed(3)).join(', ');
const toLight = vec3.normalize(vec3.create(), [1, 2, 2]); // plain arrays work as inputs
const across = [0.42, 0, 0], up = [0, 0.98, 0]; // two edges of a cover
const normal = vec3.normalize(vec3.create(), vec3.cross(vec3.create(), across, up));
console.log('toLight', r(toLight), '| normal', r(normal), '| cos', vec3.dot(toLight, normal));
const eye = [1.1, 0.9, 2.9], book = [-0.26, 0, 0];
console.log('distance', vec3.distance(eye, book).toFixed(3),
'| halfway', r(vec3.lerp(vec3.create(), eye, book, 0.5)));toLight 0.333, 0.667, 0.667 | normal 0, 0, 1 | cos 0.6666666865348816 distance 3.327 | halfway 0.42, 0.45, 1.45
The cross of the cover's edges is its outward normal, +z; the dot of two unit vectors is the cosine diffuse lighting uses (Lighting Equation), stored in a Float32Array to about seven digits, as on the GPU. Note that vec3.transformMat4() treats its input as a point and divides by w; move directions with transformMat3().
<!doctype html>
<style>
body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
canvas { display: block; width: 100%; max-width: 600px; }
pre { max-width: 600px; margin: 4px 0 0; padding: 6px 8px; background: #fff; border: 1px solid #ddd; }
</style>
<script src="https://cdn.jsdelivr.net/npm/gl-matrix@3.4.4/gl-matrix-min.js"></script>
<canvas id="view" width="600" height="260"></canvas>
<pre id="out"></pre>
<script>
const { vec3, mat4 } = glMatrix;
const gl = document.getElementById('view').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec3 aPosition;
uniform mat4 uViewProjection;
void main() { gl_Position = uViewProjection * vec4(aPosition, 1.0); gl_PointSize = 10.0; }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; uniform vec3 uColor; out vec4 c; void main() { c = vec4(uColor, 1.0); }`]]) {
const s = gl.createShader(type);
gl.shaderSource(s, src); gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);
const uColor = gl.getUniformLocation(program, 'uColor');
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
gl.enable(gl.DEPTH_TEST);
function draw(mode, points, color) {
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points.flatMap(p => [...p])), gl.DYNAMIC_DRAW);
gl.uniform3fv(uColor, color);
gl.drawArrays(mode, 0, points.length);
}
const viewProjection = mat4.perspective(mat4.create(), Math.PI / 4, 600 / 260, 0.1, 20);
mat4.multiply(viewProjection, viewProjection, mat4.lookAt(mat4.create(), [1.8, 1.2, 3.2], [0, 0.1, 0], [0, 1, 0]));
gl.uniformMatrix4fv(gl.getUniformLocation(program, 'uViewProjection'), false, viewProjection);
// Two edges of a cover; their cross product is the outward normal.
const corner = [-0.21, -0.49, 0], across = [0.42, 0, 0], up = [0, 0.98, 0];
const normal = vec3.normalize(vec3.create(), vec3.cross(vec3.create(), across, up));
const toLight = vec3.create(), tip = vec3.create(), scratch = vec3.create(); // reused every frame
const r = (v) => [...v].map((c) => c.toFixed(3)).join(', ');
function frame(ms) {
const a = ms * 0.0008;
vec3.normalize(toLight, [Math.sin(a) * 2, 1.2, Math.cos(a) * 2]); // plain arrays work as inputs
const cos = vec3.dot(toLight, normal); // the diffuse factor
const shade = 0.25 + 0.75 * Math.max(cos, 0);
gl.clearColor(0.97, 0.96, 0.93, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const c0 = corner, c1 = vec3.add(vec3.create(), corner, across);
const c2 = vec3.add(vec3.create(), c1, up), c3 = vec3.add(vec3.create(), corner, up);
draw(gl.TRIANGLES, [c0, c1, c2, c0, c2, c3], [0.12 * 2.2 * shade, 0.37 * 2.2 * shade, 0.55 * 1.6 * shade]);
draw(gl.LINES, [[0, 0, 0.001], vec3.scale(tip, normal, 0.9)], [0.25, 0.49, 0.23]); // normal (green)
draw(gl.LINES, [[0, 0, 0.001], vec3.scale(scratch, toLight, 1.3)], [0.88, 0.6, 0.06]); // to light (gold)
draw(gl.POINTS, [vec3.scale(scratch, toLight, 1.3)], [0.88, 0.6, 0.06]);
// A floor grid for depth cues.
const grid = [];
for (let i = -3; i <= 3; i++) grid.push([i * 0.4, -0.5, -1.2], [i * 0.4, -0.5, 1.2], [-1.2, -0.5, i * 0.4], [1.2, -0.5, i * 0.4]);
draw(gl.LINES, grid, [0.8, 0.78, 0.72]);
document.getElementById('out').textContent =
`normal = normalize(cross(across, up)) = ${r(normal)}\n` +
`toLight = ${r(toLight)}\n` +
`dot(toLight, normal) = ${cos.toFixed(3)} -> cover brightness ${shade.toFixed(2)}`;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>