A WebGL 3D Cube From First Principles — Handwritten 4x4 Matrices and Visible Shaders
Most WebGL tutorials import gl-matrix. This one doesn't. The 4x4 matrix math is written out: identity, multiply, translate, rotateX/Y/Z, scale, perspective, lookAt, invert. The shaders are visible in a panel. The projection × view × model matrix multiplication is shown live as you drag the cube. If you want to actually understand what WebGL is doing, gl-matrix is a black box. Removing it reveals the whole thing.
WebGL's reputation for being hard comes from the combination of verbose API + implicit math. If you've never written a matrix multiply, rotating a cube feels like magic. Writing the math yourself turns it from magic into arithmetic.
🔗 Live demo: https://sen.ltd/portfolio/cube-3d/
📦 GitHub: https://github.com/sen-ltd/cube-3d
Features:
- Pure WebGL 1.0 (no libraries)
- Handwritten 4x4 matrix library (12 functions)
- Mouse drag to rotate, scroll to zoom
- Wireframe overlay toggle
- Live matrix display (projection, view, model)
- Visible shader source code
- 3 color modes (solid, gradient, monochrome)
- Japanese / English UI
- Zero dependencies, 34 tests
Column-major 4x4 matrices
WebGL expects column-major storage. Matrix[0..3] is the first column, not the first row:
export function identity() {
return new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1,
]);
}
Reading this as rows is wrong. The four columns are: (1,0,0,0), (0,1,0,0), (0,0,1,0), (0,0,0,1). OpenGL conventions stretch back to the 80s and everyone just accepted column-major even though row-major is more natural to read.
Matrix multiplication
export function multiply(a, b) {
const out = new Float32Array(16);
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
let sum = 0;
for (let k = 0; k < 4; k++) {
// a is column-major: a[col*4 + row] = a[k*4 + i]
sum += a[k * 4 + i] * b[j * 4 + k];
}
out[j * 4 + i] = sum;
}
}
return out;
}
16 multiplies and 12 additions per entry, 64 entries total — a fixed ~256 operations. Fast enough for every frame. The indexing looks backwards because column-major: a[col * 4 + row] not a[row * 4 + col].
Rotation around an arbitrary axis
Rodrigues's rotation formula expressed as a matrix:
export function rotate(m, rad, x, y, z) {
const len = Math.sqrt(x*x + y*y + z*z);
x /= len; y /= len; z /= len;
const c = Math.cos(rad);
const s = Math.sin(rad);
const t = 1 - c;
const r = new Float32Array([
t*x*x + c, t*x*y + s*z, t*x*z - s*y, 0,
t*x*y - s*z, t*y*y + c, t*y*z + s*x, 0,
t*x*z + s*y, t*y*z - s*x, t*z*z + c, 0,
0, 0, 0, 1,
]);
return multiply(m, r);
}
Every entry is a combination of sines, cosines, and axis components. You can derive it from "rotate around X, then Y, then Z" but the closed form is faster and numerically better.
Perspective projection
export function perspective(fov, aspect, near, far) {
const f = 1 / Math.tan(fov / 2);
return new Float32Array([
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) / (near - far), -1,
0, 0, (2 * far * near) / (near - far), 0,
]);
}
This maps a frustum (pyramid cut off at near and far) to the unit cube NDC space that WebGL rasterizes. The non-obvious parts:
-
f / aspectsquishes X to account for the canvas aspect ratio - The
-1in row 4 column 3 enables perspective divide (x/w, y/w, z/w) - The z terms map the frustum depth range to [-1, 1]
Rendering loop
function render(now) {
const dt = (now - lastTime) / 1000;
lastTime = now;
// Update model matrix with rotation
let model = identity();
model = rotateY(model, rotationY);
model = rotateX(model, rotationX);
// Upload matrices to shader uniforms
gl.uniformMatrix4fv(uModel, false, model);
gl.uniformMatrix4fv(uView, false, view);
gl.uniformMatrix4fv(uProjection, false, projection);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
requestAnimationFrame(render);
}
Three matrix uniforms, one draw call, 36 indices (6 faces × 2 triangles × 3 vertices). The shader multiplies projection * view * model * position for each vertex.
Series
This is entry #74 in my 100+ public portfolio series.
- 📦 Repo: https://github.com/sen-ltd/cube-3d
- 🌐 Live: https://sen.ltd/portfolio/cube-3d/
- 🏢 Company: https://sen.ltd/

Top comments (0)