drag empty space: sling a random convex shape in — the line shows the throw drag a shape: grab it at the point you clicked, so pulling a box by one corner spins it ↑ / ↓: solver iterations, 1 to 16 — the ticks along the bottom-left W: warm starting on/off (the amber bar). Turn it off and watch D: draw contact points and normals SPACE: pause P: 21-box pyramid · T — seven-box tower · C — clear · R — reset
A 2D rigid body engine — SAT collision detection, face clipping for real contact manifolds, and warm-started sequential impulses with friction and correct angular response. Throw shapes at a pyramid and it holds. THE INTERESTING BIT Rotation is what makes this hard, and almost every design decision follows from it. A box resting flat on the floor does not touch it at a point, it touches along an edge — and if collision detection reports one point (the deepest vertex, say, which is the obvious thing to do after SAT) the box is resting on a pin and nothing but friction stops it pivoting. So after SAT finds the axis of least penetration, the incident face is **clipped against the reference face's two side planes**, which returns the two points a flat rest really has, both carrying the same normal. That is the difference between a stack and a heap. The other half is that contacts are coupled: 21 boxes in a pyramid are one linear complementarity problem, and solving them one at a time only converges if each contact starts from the impulse it needed last frame. Measured on the pyramid this project ships, over 600 frames: | | residual slide | mean overlap | worst box moved | |---|---|---|---| | warm starting off, 4 iterations | 13.2 px/s | 1.09 px | collapsed and scattered | | warm starting off, 8 iterations | 1.02 px/s | 1.11 px | 157 px | | warm starting on, 8 iterations | 0.000 px/s | 0.29 px | 1.9 px | | warm starting on, 14 iterations | 0.000 px/s | 0.19 px | 1.5 px | Press W in the running project to see that difference live. The 0.000 is not rounding-down of something small — the pyramid is genuinely, bit-for-bit still, and the figures above were confirmed inside Scratch as well as in the Python reference: 70 contacts, every velocity exactly zero, six rows within 0.3px of each other. Two smaller findings, both of which cost a measurement to notice: * Positional correction is done with split impulses, not a Baumgarte bias. Pushing overlap out by adding a separating velocity to the normal constraint works, but that velocity stays in the body and the stack has to absorb it afterwards; a seven-box tower solved that way chatters at 27 px/s forever. Routing the correction through a second, parallel pseudo-velocity field that only ever reaches the position integrator leaves the same overlap (0.45px) and 0.000 px/s of motion. With no positional term at all the tower sinks to 9.2px of overlap — a third of a box thickness. * A forward-only Gauss-Seidel sweep walks a tower over. Solving a manifold's first contact point before its second every single time favours the same corner of every box, and the leftover torque all points one way: 30px of sideways creep over 700 frames. Alternating the sweep direction on odd and even iterations takes that to 3.6px and costs one comparison. Mass and inertia are computed from the polygon itself (signed area, area- weighted centroid, the standard second-moment sum) rather than looked up per shape, and the vertices are re-expressed about that centroid — rotate a shape about anything but its centre of mass and it orbits instead of spinning. HONEST LIMITATIONS * No continuous collision detection. A small shape thrown at full strength can tunnel through a wall in one step. Anything that escapes the stage is recycled back to the top rather than left to accumulate velocity forever. All original - code, art and sound. See Inside is open.