The robot playground has no WebGL, no three.js, no matrix library and no shaders. It
draws every frame with ctx.fill() on a 2D canvas, which is how real-time 3D worked before
GPUs were something you could assume. The whole renderer is about 120 lines, and writing it that way
makes the pipeline legible in a way a shader never is.
Four steps, in order
Every frame does the same four things to a list of flat-shaded quads:
That is the entire pipeline. Each step is a few lines, and each one is doing a job that a GPU would do in dedicated silicon.
Project
Rotate the point into camera space, then divide by depth. Perspective really is just division — things twice as far away are half the size — and the rest of the projection matrix formalism exists to make that composable, not to make it different.
Cull
A face pointing away from the camera is on the far side of a closed body and cannot be seen, so it is discarded before any further work:
if (dot(n, sb(eye, cen)) <= 0) continue; // normal faces away → skip
For a closed convex body this removes about half the geometry for the cost of one dot product per face. It is also what makes the painter's algorithm survive the next step, because the faces most likely to produce ordering artefacts — the back ones — are already gone.
Shade
One colour per face, from the angle between its normal and a fixed light direction:
d = 0.42 + 0.62 * max(0, dot(n, LIGHT))
That is Lambert's cosine law with an ambient floor. The 0.42 is the ambient term —
what a surface facing away from the light still receives — and the 0.62 scales the
diffuse response. No smooth normals, no interpolation across the face, so every quad is a single flat
colour. The result reads as deliberately faceted rather than as broken, which is the aesthetic the
whole sandbox is built around.
Sort
With no depth buffer, correctness is entirely a question of paint order: compute each face's mean depth, sort descending, fill in that order. Later paint covers earlier paint.
Bodies are nested frames
Each robot is built from boxes in nested coordinate frames: a thigh is positioned relative to the hip, a shin relative to the knee. Animating a joint means changing one angle and letting everything below inherit the transform — the same idea as a scene graph, expressed as function calls rather than a data structure.
Gaits are procedural, not keyframed. Legs swing on sine waves offset in phase; a quadruped's diagonal pairs move together; the drone tilts into its acceleration. This is a few trigonometric functions rather than an animation system, and it is why swapping the body — humanoid, quadruped, rover, arm, drone — changes the feel so completely. The controller barely changes. The morphology does all the work, which is the point the sandbox exists to make.
Why bother, when WebGL exists
Three reasons, in ascending order of honesty.
It has zero dependencies and works everywhere a canvas does, including contexts where WebGL is blocked or unavailable. It is small enough to read in one sitting — the whole pipeline is on one screen, whereas the equivalent WebGL version buries half of it in GLSL and the other half in buffer setup. And the failure modes are legible: get the cull backwards and the robot turns inside out; get the sort backwards and it paints itself hidden. Those are the kinds of bugs you learn from, because they are visible in the output rather than in a validation layer.
The cost is a hard ceiling on geometry. A few hundred faces at 60fps is fine; a few thousand is not. That constraint shaped the visual design more than any aesthetic decision did.