EntroπaLabs
Robotics · Embodiment

A 3D renderer in 120 lines of canvas

No WebGL, no matrices library, no dependencies: project, cull, shade, sort. The robot playground draws every frame the way graphics worked before GPUs.

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:

1. project 3D point → 2D screen position 2. cull drop faces pointing away from the camera 3. shade one colour per face, from its normal 4. sort paint far → near, then fill

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.

The painter's algorithm is wrong, and here it doesn't matter. Sorting by average depth fails for interpenetrating or long oblique faces — the classic case is three triangles overlapping in a cycle, where no ordering is correct. This scene is made of small convex boxes that rarely interpenetrate, so the failure case is mostly designed out rather than solved. A depth buffer would fix it properly, at the cost of per-pixel work that a 2D canvas cannot do.

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.

Open the sandbox